MVVM 导航框架
大约 9 分钟约 2583 字
MVVM 导航框架
简介
WPF 中 MVVM 模式的导航是一个核心问题。与传统 WinForms 直接切换窗体不同,MVVM 要求 View 和 ViewModel 解耦。常见的导航方案包括 DataTemplate 切换、Prism 导航、CommunityToolkit 导航等。
特点
简单导航 — ContentControl + DataTemplate
基本原理
<!-- MainWindow.xaml — 使用 ContentControl 作为导航容器 -->
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 导航菜单 -->
<StackPanel Grid.Column="0">
<Button Content="首页" Command="{Binding NavigateCommand}"
CommandParameter="Home" Margin="5"/>
<Button Content="用户管理" Command="{Binding NavigateCommand}"
CommandParameter="Users" Margin="5"/>
<Button Content="订单管理" Command="{Binding NavigateCommand}"
CommandParameter="Orders" Margin="5"/>
<Button Content="系统设置" Command="{Binding NavigateCommand}"
CommandParameter="Settings" Margin="5"/>
</StackPanel>
<!-- 内容区域 — 绑定 CurrentView -->
<ContentControl Grid.Column="1" Content="{Binding CurrentView}"/>
</Grid>
</Window>/// <summary>
/// 简单导航 ViewModel
/// </summary>
public class MainViewModel : ObservableObject
{
private object? _currentView;
public object? CurrentView
{
get => _currentView;
set => SetProperty(ref _currentView, value);
}
public ICommand NavigateCommand { get; }
public MainViewModel()
{
NavigateCommand = new RelayCommand<string>(Navigate);
CurrentView = new HomeViewModel(); // 默认页面
}
private void Navigate(string? viewName)
{
CurrentView = viewName switch
{
"Home" => new HomeViewModel(),
"Users" => new UserListViewModel(),
"Orders" => new OrderListViewModel(),
"Settings" => new SettingsViewModel(),
_ => new HomeViewModel()
};
}
}DataTemplate 映射
<!-- App.xaml — 自动将 ViewModel 映射到 View -->
<Application.Resources>
<DataTemplate DataType="{x:Type viewmodels:HomeViewModel}">
<views:HomeView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:UserListViewModel}">
<views:UserListView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:OrderListViewModel}">
<views:OrderListView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:SettingsViewModel}">
<views:SettingsView/>
</DataTemplate>
</Application.Resources>导航服务 — DI 集成
依赖注入导航
/// <summary>
/// 导航服务 — 通过 DI 容器创建 ViewModel
/// </summary>
public class NavigationService : ObservableObject, INavigationService
{
private readonly IServiceProvider _serviceProvider;
private object? _currentView;
public object? CurrentView
{
get => _currentView;
private set => SetProperty(ref _currentView, value);
}
public NavigationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void NavigateTo<TViewModel>() where TViewModel : class
{
var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
CurrentView = viewModel;
}
public void NavigateTo<TViewModel>(object parameter) where TViewModel : class
{
var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
// 如果 ViewModel 实现了 INavigationAware,传递参数
if (viewModel is INavigationAware aware)
{
aware.OnNavigatedTo(parameter);
}
CurrentView = viewModel;
}
}
// 导航感知接口
public interface INavigationAware
{
void OnNavigatedTo(object parameter);
void OnNavigatedFrom();
}DI 注册
/// <summary>
/// 在 App.xaml.cs 中配置 DI
/// </summary>
public partial class App : Application
{
private IServiceProvider _serviceProvider = null!;
protected override void OnStartup(StartupEventArgs e)
{
var services = new ServiceCollection();
// 注册 ViewModels
services.AddTransient<MainViewModel>();
services.AddTransient<HomeViewModel>();
services.AddTransient<UserListViewModel>();
services.AddTransient<OrderListViewModel>();
services.AddTransient<UserDetailViewModel>();
// 注册导航服务(单例)
services.AddSingleton<INavigationService, NavigationService>();
// 注册其他服务
services.AddSingleton<IUserService, UserService>();
services.AddSingleton<IOrderService, OrderService>();
_serviceProvider = services.BuildServiceProvider();
// 显示主窗口
var mainWindow = new MainWindow
{
DataContext = _serviceProvider.GetRequiredService<MainViewModel>()
};
mainWindow.Show();
base.OnStartup(e);
}
}带参数的导航
ViewModel 实现
/// <summary>
/// 带参数导航 — 详情页面
/// </summary>
public class UserDetailViewModel : ObservableObject, INavigationAware
{
private readonly IUserService _userService;
private UserDto? _user;
public UserDto? User
{
get => _user;
set => SetProperty(ref _user, value);
}
public UserDetailViewModel(IUserService userService)
{
_userService = userService;
}
// 导航到达时接收参数
public async void OnNavigatedTo(object parameter)
{
if (parameter is int userId)
{
User = await _userService.GetByIdAsync(userId);
}
}
public void OnNavigatedFrom()
{
// 离开页面时的清理
}
}
// 从列表页导航到详情页
public class UserListViewModel : ObservableObject
{
private readonly INavigationService _navigation;
public ICommand ViewDetailCommand { get; }
public UserListViewModel(INavigationService navigation)
{
_navigation = navigation;
ViewDetailCommand = new RelayCommand<int>(id =>
_navigation.NavigateTo<UserDetailViewModel>(id));
}
}<!-- UserListView.xaml -->
<ListBox ItemsSource="{Binding Users}" SelectedItem="{Binding SelectedUser}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="0,0,10,0"/>
<Button Content="查看详情"
Command="{Binding DataContext.ViewDetailCommand,
RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding Id}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>Frame 导航
基于 Frame 的页面导航
<!-- MainWindow.xaml -->
<Grid>
<Frame x:Name="MainFrame" NavigationUIVisibility="Hidden"/>
</Grid>/// <summary>
/// Frame 导航 — 类似 Web 的页面导航
/// </summary>
public class FrameNavigationService : INavigationService
{
private readonly Frame _frame;
public FrameNavigationService(Frame frame)
{
_frame = frame;
}
public void NavigateTo<TPage>() where TPage : class
{
_frame.Navigate(typeof(TPage));
}
public void NavigateTo<TPage>(object parameter) where TPage : class
{
_frame.Navigate(typeof(TPage), parameter);
}
public void GoBack()
{
if (_frame.CanGoBack)
_frame.GoBack();
}
}
// Page 中使用
public partial class UserListPage : Page
{
public UserListPage()
{
InitializeComponent();
}
private void ViewDetail_Click(object sender, RoutedEventArgs e)
{
NavigationService.Navigate(new UserDetailPage(userId));
}
}TabControl 导航
选项卡式导航
/// <summary>
/// TabControl 导航 — 多标签页
/// </summary>
public class TabNavigationViewModel : ObservableObject
{
private readonly IServiceProvider _serviceProvider;
public ObservableCollection<TabItem> Tabs { get; } = new();
private TabItem? _selectedTab;
public TabItem? SelectedTab
{
get => _selectedTab;
set => SetProperty(ref _selectedTab, value);
}
public ICommand CloseTabCommand { get; }
public ICommand OpenTabCommand { get; }
public TabNavigationViewModel(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
CloseTabCommand = new RelayCommand<TabItem>(CloseTab);
OpenTabCommand = new RelayCommand<string>(OpenTab);
}
private void OpenTab(string? tabType)
{
// 检查是否已打开
var existing = Tabs.FirstOrDefault(t => t.Header == tabType);
if (existing != null)
{
SelectedTab = existing;
return;
}
var content = tabType switch
{
"Users" => _serviceProvider.GetRequiredService<UserListViewModel>(),
"Orders" => _serviceProvider.GetRequiredService<OrderListViewModel>(),
_ => null
};
if (content == null) return;
var tab = new TabItem { Header = tabType, Content = content };
Tabs.Add(tab);
SelectedTab = tab;
}
private void CloseTab(TabItem? tab)
{
if (tab != null)
{
Tabs.Remove(tab);
}
}
}
public class TabItem
{
public string Header { get; set; } = "";
public object Content { get; set; } = null!;
}导航方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ContentControl+DataTemplate | 简单,WPF 原生 | 无导航栈 | 单页切换 |
| NavigationService + DI | 解耦,支持参数 | 需要配置 DI | 中大型项目 |
| Frame 导航 | 支持前进/后退 | Page 生命周期复杂 | 文档型应用 |
| TabControl | 多页面并存 | 内存占用 | IDE/管理后台 |
| Prism 导航 | 功能完整 | 依赖 Prism | 企业级项目 |
导航动画
页面切换动画实现
/// <summary>
/// 页面切换动画帮助类
/// </summary>
public static class NavigationAnimation
{
// 淡入淡出动画
public static async Task FadeTransition(FrameworkElement oldView, FrameworkElement newView)
{
if (oldView != null)
{
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(200));
oldView.BeginAnimation(UIElement.OpacityProperty, fadeOut);
await Task.Delay(200);
}
if (newView != null)
{
newView.Opacity = 0;
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200));
newView.BeginAnimation(UIElement.OpacityProperty, fadeIn);
}
}
// 滑动动画
public static async Task SlideTransition(
FrameworkElement oldView, FrameworkElement newView, bool toRight = true)
{
var translateTransform = new TranslateTransform();
newView.RenderTransform = translateTransform;
newView.Opacity = 0;
// 新页面从右侧滑入
var slideAnimation = new DoubleAnimation(
toRight ? 300 : -300, 0,
TimeSpan.FromMilliseconds(300))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(250));
if (oldView != null)
{
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(200));
oldView.BeginAnimation(UIElement.OpacityProperty, fadeOut);
}
translateTransform.BeginAnimation(TranslateTransform.XProperty, slideAnimation);
newView.BeginAnimation(UIElement.OpacityProperty, fadeIn);
await Task.Delay(300);
}
}带动画的导航服务
/// <summary>
/// 带过渡动画的导航服务
/// </summary>
public class AnimatedNavigationService : ObservableObject, INavigationService
{
private readonly IServiceProvider _serviceProvider;
private ContentControl? _host;
private object? _currentView;
public object? CurrentView
{
get => _currentView;
private set => SetProperty(ref _currentView, value);
}
public AnimatedNavigationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void SetHost(ContentControl host)
{
_host = host;
}
public async void NavigateTo<TViewModel>() where TViewModel : class
{
var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
await NavigateWithAnimation(viewModel);
}
public async void NavigateTo<TViewModel>(object parameter) where TViewModel : class
{
var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
if (viewModel is INavigationAware aware)
{
aware.OnNavigatedTo(parameter);
}
await NavigateWithAnimation(viewModel);
}
private async Task NavigateWithAnimation(object newViewModel)
{
if (_host == null) return;
var oldContent = _host.Content as FrameworkElement;
CurrentView = newViewModel;
// 等待新视图渲染
await Application.Current.Dispatcher.InvokeAsync(() =>
{
_host.Content = newViewModel;
}, DispatcherPriority.Loaded);
var newContent = _host.Content as FrameworkElement;
// 执行过渡动画
await NavigationAnimation.SlideTransition(oldContent, newContent);
}
}Prism 导航简介
Prism 区域导航
// Prism 框架提供了更完整的导航解决方案
// 安装 Prism.DryIoc(或 Prism.Unity)NuGet 包
// 1. App.xaml.cs — Prism 应用基类
public partial class App : PrismApplication
{
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册导航页面
containerRegistry.RegisterForNavigation<MainWindow>();
containerRegistry.RegisterForNavigation<HomeView, HomeViewModel>();
containerRegistry.RegisterForNavigation<UserListView, UserListViewModel>();
containerRegistry.RegisterForNavigation<UserDetailView, UserDetailViewModel>();
containerRegistry.RegisterForNavigation<SettingsView, SettingsViewModel>();
// 注册服务
containerRegistry.AddSingleton<IUserService, UserService>();
}
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void OnInitialized()
{
// 初始导航
var regionManager = Container.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("ContentRegion", typeof(HomeView));
}
}
// 2. MainWindow.xaml — 定义导航区域
// <ContentControl prism:RegionManager.RegionName="ContentRegion" />
// 3. ViewModel 中导航
public class UserListViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
public DelegateCommand<int> ViewDetailCommand { get; }
public UserListViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
ViewDetailCommand = new DelegateCommand<int>(ViewDetail);
}
private void ViewDetail(int userId)
{
// 导航并传递参数
var parameters = new NavigationParameters
{
{ "userId", userId }
};
_regionManager.RequestNavigate("ContentRegion", "UserDetailView", parameters);
}
}
// 4. 目标页面接收参数
public class UserDetailViewModel : BindableBase, INavigationAware
{
private readonly IUserService _userService;
public void OnNavigatedTo(NavigationContext navigationContext)
{
var userId = navigationContext.Parameters.GetValue<int>("userId");
LoadUser(userId);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
// 返回 true 表示复用现有实例,false 表示创建新实例
var userId = navigationContext.Parameters.GetValue<int>("userId");
return _user?.Id == userId;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
// 离开页面时的清理
}
}优点
缺点
总结
WPF MVVM 导航推荐使用 ContentControl + DataTemplate + DI 方案,简单且解耦彻底。小型项目直接 ContentControl 切换,中大型项目用 NavigationService + DI。核心原则:ViewModel 负责"去哪里",View 只负责"怎么显示"。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《MVVM 导航框架》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《MVVM 导航框架》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《MVVM 导航框架》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《MVVM 导航框架》最大的收益和代价分别是什么?
