UWP应用中Page-Frame与ContentPresenter选型及MVVM导航方案咨询
UWP MVVM视图导航与项目结构指南
一、MainPage替代MainWindow的设计逻辑
UWP从设计之初就面向跨设备场景(手机、平板、PC等),没有WPF中独立的MainWindow概念,而是用Page作为核心容器,MainPage是默认的启动入口,相当于WPF里的主窗口,但它能更好地适配不同设备的窗口尺寸、旋转状态。所有视图切换都基于Page或嵌套的UserControl完成,这是UWP的标准设计,按此逻辑实现即可。
二、MVVM模式下的视图导航最佳实践
UWP中推荐用ContentControl + ViewModel驱动 + DataTemplate匹配的方式,和WPF的ContentPresenter思路一致,核心逻辑:
- 用
ContentControl作为主视图容器,绑定到ViewModel中的CurrentViewModel属性 - 为每个子ViewModel定义对应的
DataTemplate,让UWP自动根据ViewModel类型渲染对应的UserControl - 通过导航服务切换
CurrentViewModel,视图仅绑定数据,不处理导航逻辑,完全符合MVVM
核心组件实现
1. 导航服务(INavigationService)
封装导航逻辑,避免ViewModel依赖视图:
public interface INavigationService { void NavigateTo<TViewModel>() where TViewModel : ViewModelBase; void NavigateTo<TViewModel>(object parameter) where TViewModel : ViewModelBase; } public class NavigationService : INavigationService { private readonly MainViewModel _mainViewModel; private readonly Func<Type, ViewModelBase> _viewModelFactory; public NavigationService(MainViewModel mainViewModel, Func<Type, ViewModelBase> viewModelFactory) { _mainViewModel = mainViewModel; _viewModelFactory = viewModelFactory; } public void NavigateTo<TViewModel>() => NavigateTo<TViewModel>(null); public void NavigateTo<TViewModel>(object parameter) { var viewModel = _viewModelFactory(typeof(TViewModel)) as ViewModelBase; viewModel?.SetParameter(parameter); _mainViewModel.CurrentViewModel = viewModel; } }
2. ViewModel基类
实现INotifyPropertyChanged,所有ViewModel继承此类:
public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // 接收导航参数的虚方法 public virtual void SetParameter(object parameter) { } }
3. MainViewModel(主视图ViewModel)
维护当前激活的ViewModel:
public class MainViewModel : ViewModelBase { private ViewModelBase _currentViewModel; public ViewModelBase CurrentViewModel { get => _currentViewModel; set { _currentViewModel = value; OnPropertyChanged(); } } public INavigationService NavigationService { get; } public MainViewModel(INavigationService navigationService) { NavigationService = navigationService; // 初始导航到首页ViewModel NavigationService.NavigateTo<HomeViewModel>(); } }
三、重写App.xaml.cs的OnLaunched方法
初始化MainPage,注入ViewModel和导航服务:
protected override void OnLaunched(LaunchActivatedEventArgs e) { Frame rootFrame = Window.Current.Content as Frame; if (rootFrame == null) { rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) { // 此处添加应用挂起后的状态恢复逻辑 } Window.Current.Content = rootFrame; } if (e.PrelaunchActivated == false) { if (rootFrame.Content == null) { // 手动实例化ViewModel和导航服务(实际项目可改用依赖注入框架) var mainViewModel = new MainViewModel(new NavigationService( null, type => { return type switch { Type t when t == typeof(HomeViewModel) => new HomeViewModel(), Type t when t == typeof(UserManageViewModel) => new UserManageViewModel(), _ => null }; })); rootFrame.Navigate(typeof(MainPage), mainViewModel); } Window.Current.Activate(); } } private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) { throw new Exception($"加载页面失败:{e.SourcePageType.FullName}"); }
四、视图层实现(XAML)
MainPage.xaml
用ContentControl绑定CurrentViewModel,定义ViewModel与UserControl的匹配模板:
<Page x:Class="UwpMvvmDemo.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:UwpMvvmDemo" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Page.DataContext> <local:MainViewModel/> </Page.DataContext> <Grid> <!-- 侧边导航按钮 --> <StackPanel Orientation="Horizontal" Margin="10"> <Button Content="首页" Command="{Binding NavigationService, Converter={local:NavigationCommandConverter {x:Type local:HomeViewModel}}}" Margin="0,0,10,0"/> <Button Content="用户管理" Command="{Binding NavigationService, Converter={local:NavigationCommandConverter {x:Type local:UserManageViewModel}}}"/> </StackPanel> <!-- 主视图容器 --> <ContentControl Content="{Binding CurrentViewModel}" Margin="0,50,0,0"> <ContentControl.Resources> <DataTemplate DataType="{x:Type local:HomeViewModel}"> <local:HomeUserControl/> </DataTemplate> <DataTemplate DataType="{x:Type local:UserManageViewModel}"> <local:UserManageUserControl/> </DataTemplate> </ContentControl.Resources> </ContentControl> </Grid> </Page>
辅助转换器(NavigationCommandConverter)
将导航服务转为ICommand,实现按钮点击导航:
public class NavigationCommandConverter : IValueConverter { public Type ViewModelType { get; set; } public object Convert(object value, Type targetType, object parameter, string language) { var navigationService = value as INavigationService; return new RelayCommand(() => navigationService?.NavigateTo(ViewModelType)); } public object ConvertBack(object value, Type targetType, object parameter, string language) { throw new NotImplementedException(); } } // 标准RelayCommand实现 public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public event EventHandler CanExecuteChanged; public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; public void Execute(object parameter) => _execute(); public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
五、CRUD示例(用户管理)
1. UserManageViewModel
public class UserManageViewModel : ViewModelBase { private ObservableCollection<User> _users; public ObservableCollection<User> Users { get => _users; set { _users = value; OnPropertyChanged(); } } private User _selectedUser; public User SelectedUser { get => _selectedUser; set { _selectedUser = value; OnPropertyChanged(); if (value != null) { EditName = value.Name; EditAge = value.Age; } } } private string _editName; public string EditName { get => _editName; set { _editName = value; OnPropertyChanged(); } } private int _editAge; public int EditAge { get => _editAge; set { _editAge = value; OnPropertyChanged(); } } public ICommand AddCommand { get; } public ICommand UpdateCommand { get; } public ICommand DeleteCommand { get; } public UserManageViewModel() { // 模拟初始化数据 Users = new ObservableCollection<User> { new User { Id = 1, Name = "张三", Age = 25 }, new User { Id = 2, Name = "李四", Age = 30 } }; AddCommand = new RelayCommand(AddUser); UpdateCommand = new RelayCommand(UpdateUser, () => SelectedUser != null); DeleteCommand = new RelayCommand(DeleteUser, () => SelectedUser != null); } private void AddUser() { var newUser = new User { Id = Users.Max(u => u.Id) + 1, Name = EditName, Age = EditAge }; Users.Add(newUser); EditName = string.Empty; EditAge = 0; } private void UpdateUser() { SelectedUser.Name = EditName; SelectedUser.Age = EditAge; } private void DeleteUser() { if (SelectedUser != null) { Users.Remove(SelectedUser); SelectedUser = null; } } } // User实体类 public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
2. UserManageUserControl.xaml
<UserControl x:Class="UwpMvvmDemo.UserManageUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:UwpMvvmDemo" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 编辑操作区 --> <StackPanel Orientation="Horizontal" Margin="10"> <TextBox PlaceholderText="姓名" Text="{Binding EditName, Mode=TwoWay}" Width="150" Margin="0,0,10,0"/> <TextBox PlaceholderText="年龄" Text="{Binding EditAge, Mode=TwoWay}" Width="100" Margin="0,0,10,0"/> <Button Content="添加" Command="{Binding AddCommand}"/> <Button Content="更新" Command="{Binding UpdateCommand}" Margin="10,0,0,0"/> <Button Content="删除" Command="{Binding DeleteCommand}" Margin="10,0,0,0"/> </StackPanel> <!-- 用户列表区 --> <ListView ItemsSource="{Binding Users}" SelectedItem="{Binding SelectedUser, Mode=TwoWay}" Grid.Row="1" Margin="10"> <ListView.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Id}" Width="50"/> <TextBlock Text="{Binding Name}" Width="150"/> <TextBlock Text="{Binding Age}" Width="100"/> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </UserControl>
内容的提问来源于stack exchange,提问作者JoJo
相关产品推荐
相关产品推荐

