WPF中在MVVM模式下实现导航功能

WPF中在MVVM模式下实现导航功能

一、利用TabControl

使用场景:项目小,不用考虑内存开销的问题。

实现方式1-手动指定ViewModel

  1. 分别定义3个UserControl作为View用于演示
xml 复制代码
 <UserControl
     ...>
     <Grid>
         <StackPanel Orientation="Vertical">
             <TextBlock
                 HorizontalAlignment="Center"
                 VerticalAlignment="Top"
                 Text="Page 1" />
             <TextBlock
                 d:Text="Page 1"
                 FontSize="50"
                 Text="{Binding PageMessage}" />
         </StackPanel>
     </Grid>
 </UserControl>
  1. 分别定义ViewModel
csharp 复制代码
 public abstract class PageViewModelBase 
 {
     public string? Header { get; set; }
 }
 public class MainViewModel 
 {
     public List<PageViewModelBase> ViewModels { get; }
 
     public MainViewModel(Page1ViewModel p1, Page2ViewModel p2, Page3ViewModel p3)
     {
         ViewModels = new List<PageViewModelBase> { p1, p2, p3 };
     }
 }
 public class Page1ViewModel : PageViewModelBase
 {
     public Page1ViewModel() => Header = "Page 1";
 
     public string PageMessage { get; set; } = "Hello, Page 1";
 }
 
 public class Page2ViewModel : PageViewModelBase
 {
     public Page2ViewModel() => Header = "Page 2";
 
     public string PageMessage { get; set; } = "Hello, Page 2";
 }
 
 public class Page3ViewModel : PageViewModelBase
 {
     public Page3ViewModel() => Header = "Page 3";
 
     public string PageMessage { get; set; } = "Hello, Page 3";
 }
  1. 在MainWindow上定义Tabcontrol
xml 复制代码
 <Window
            ...>
     <Grid>
         <TabControl ItemsSource="{Binding ViewModels}">
             <TabItem Header="Pag1">
                 <view:Page1>
                     <view:Page1.DataContext>
                         <local:Page1ViewModel />
                     </view:Page1.DataContext>
                 </view:Page1>
             </TabItem>
             <TabItem Header="Pag2">
                 <view:Page1>
                     <view:Page1.DataContext>
                         <local:Page2ViewModel />
                     </view:Page1.DataContext>
                 </view:Page1>
             </TabItem>
             <TabItem Header="Pag3">
                 <view:Page1>
                     <view:Page1.DataContext>
                         <local:Page3ViewModel />
                     </view:Page1.DataContext>
                 </view:Page1>
             </TabItem>
         </TabControl>
     </Grid>
 </Window>

这种方式需要手动指定每个View的ViewModel

实现方式2-利用ItemTemplate

  1. 在MainViewModel中声明一个ViewModel列表
csharp 复制代码
public class MainViewModel 
{
    public List<PageViewModelBase> ViewModels { get; }

    public MainViewModel(Page1ViewModel p1, Page2ViewModel p2, Page3ViewModel p3)
    {
        ViewModels = new List<PageViewModelBase> { p1, p2, p3 };
    }
}
  1. 在MainWindow中为TabControl指定ItemTemplate,上一步声明的ViewModel列表作为 TabControl 的 ItemsSource;为 TabControl.Resources 添 加多个 DataTemplate,指定 VM 对应什么样的 Page
xml 复制代码
<Window d:DataContext="{d:DesignInstance Type=local:MainViewModel}"
         ....>
    <Grid>
        <TabControl ItemsSource="{Binding ViewModels}">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Header}"/>
                </DataTemplate>
            </TabControl.ItemTemplate>
            <TabControl.Resources>
                <DataTemplate DataType="{x:Type local:Page1ViewModel}">
                    <view:Page1/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type local:Page2ViewModel}">
                    <view:Page2/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type local:Page3ViewModel}">
                    <view:Page3/>
                </DataTemplate>
            </TabControl.Resources>   
        </TabControl>
    </Grid>
</Window>

这样的好处是自动会为不同的View绑定了相应的ViewModel。

小技巧 :在xaml中加上了d:DataContext="{d:DesignInstance Type=local:MainViewModel},这样在写Binding的时候就有了智能提示。

以上两种方式均可结合依赖注入的方式来实现

二、自定义NavigationService服务

  1. 实现一个NavigationService服务,并作为单例
csharp 复制代码
 class NavigationService
 {
 	//设置一个单例服务
     public static NavigationService Instance { get; private set; } = new NavigationService();
     //声明一个事件,当更改CurrentViewModel时触发
     public event Action? CurrentViewModelChanged;
  	//设置一个当前VM的属性,并在属性改变时触发CurrentViewModelChanged
     private ViewModelBase? currentViewModel;
     public ViewModelBase? CurrentViewModel
     {
         get => currentViewModel;
         set
         {
             currentViewModel = value;
             CurrentViewModelChanged?.Invoke();
         }
     }
     //页面导航方法,给CurrentViewModel赋值,触发CurrentViewModelChanged事件
     public void NavigateTo(ViewModelBase viewModel)=>CurrentViewModel = viewModel;
 }
  1. 设置MainViewModel中的CurrentViewModel属性
csharp 复制代码
 public class ViewModelBase : ObservableObject{}
 public partial class MainViewModel : ViewModelBase
 {
     [ObservableProperty]
     private ViewModelBase? currentViewModel;//当前的VM
 
     public MainViewModel()
     {
 		//为事件绑定委托方法,设置CurrentVM和NavigationService中的CurrentVM保持一致
         NavigationService.Instance.CurrentViewModelChanged += () =>
         {
             CurrentViewModel = NavigationService.Instance.CurrentViewModel;
         };
 
         //调用导航方法
         NavigationService.Instance.NavigateTo(new LoginViewModel());
     }
 }

其他两个ViewModel分别为

csharp 复制代码
 public partial class LoginViewModel : ViewModelBase
 {
     [ObservableProperty]
     string? userName = "Sean";

     [RelayCommand]
     void Login()
     {
         NavigationService.Instance.NavigateTo(new HomeViewModel());
     }
 }
 public partial class HomeViewModel : ViewModelBase
 {
     [ObservableProperty]
     string? userName;

     [RelayCommand]
     void Logout()
     {
         NavigationService.Instance.NavigateTo(new LoginViewModel());
     }
 }
  1. 使用ContentControl作为MainWindow上不同页面载体显示内容,并借助DataTemplate来实现View和ViewModel的映射
xml 复制代码
 <Window ...>
     <ContentControl Content="{Binding CurrentViewModel}">
         <ContentControl.Resources>
             <DataTemplate DataType="{x:Type vm:LoginViewModel}">
                 <view:Login />
             </DataTemplate>
             <DataTemplate DataType="{x:Type vm:HomeViewModel}">
                 <view:Home />
             </DataTemplate>
         </ContentControl.Resources>
     </ContentControl>
 </Window>

在ContentControl.Resources中设置DataTemplate,根据DataType自动选择相应的VM,这样做的好处是会自动将View和VM进行了绑定。

改进

  1. 单例方式可以采用依赖注入的方式来实现
  2. 在NavigationService服务中,可以改进页面导航的方法
csharp 复制代码
public void NavigateTo<T>() where T : ViewModelBase
    => CurrentViewModel = App.Current.Services.GetService<T>();

//在调用导航方法时可以使用
navigationService.NavigateTo<HomeViewModel>();

三、借助ValueConverter

实现上一章节的功能,这种方法本质上是通过View来自动绑定VM。

  1. 定义Page的枚举
csharp 复制代码
 public enum ApplicationPage
 {
     Empty,
     Login,
     Home
 }
  1. 定义各ViewModel
csharp 复制代码
 public class ViewModelBase : ObservableObject{}
 public partial class MainViewModel : ViewModelBase
 {
     //MainViewModel中的CurrentPage是一个枚举类型
     [ObservableProperty]
     ApplicationPage currentPage;
 
     public MainViewModel()
     {
         CurrentPage = ApplicationPage.Login;
     }
 }
 public partial class LoginViewModel : ViewModelBase
 {
     public string UserName { get; set; } = "AngelSix";
 
     [RelayCommand]
     void Login()
     {
         var mainVM= App.Current.MainWindow.DataContext as MainViewModel;
         mainVM!.CurrentPage = ApplicationPage.Home;
     }
 }
 public partial class HomeViewModel : ViewModelBase
 {
     [RelayCommand]
     void Logout()
     {
         var mainVM = App.Current.MainWindow.DataContext as MainViewModel;
         mainVM!.CurrentPage = ApplicationPage.Login;
     }
 }
  1. 定义Page基类和各个Page

    这种方法本质上是通过View来自动绑定VM,所以在此处使用泛型

csharp 复制代码
 public abstract class BasePage<VM> : UserControl where VM : ViewModelBase, new()
 {
     public BasePage()
     {
         DataContext = new VM();
     }
 }
  • 实现Home页面

将Home.xaml.cs中的继承删掉,以为它和Home.xaml相互为分部类,只在一个分部类上实现继承就可以。

xml 复制代码
 <local:BasePage 
    x:TypeArguments="vm:HomeViewModel"
                 ...>
      <!--x:TypeArguments指定泛型-->
     <Grid>
         <TextBlock HorizontalAlignment="Center"
             VerticalAlignment="Center"
             Text="Home"
             FontSize="32" />
         <Button Margin="10" Grid.Row="1"
          HorizontalAlignment="Right"
          VerticalAlignment="Bottom"
          Content="Logout"
          Command="{Binding LogoutCommand}" />
     </Grid>
 </local:BasePage>
  • 实现Login页面

方法和实现Home页面方法相同

xml 复制代码
 <local:BasePage
     x:TypeArguments="vm:LoginViewModel" ...>
     <Grid>
         <Border
             Padding="10"
             HorizontalAlignment="Center"
             VerticalAlignment="Center"
             BorderBrush="LightGray"
             BorderThickness="1"
             CornerRadius="10">
             <StackPanel Width="300">
                 <TextBlock HorizontalAlignment="Center" FontSize="28">Login</TextBlock>
                 <Separator Margin="0,10" />
                 <TextBlock>User name:</TextBlock>
                 <TextBox
                     Margin="0,10"
                     InputMethod.IsInputMethodEnabled="False"
                     Text="{Binding UserName}" />
                 <TextBlock>Password:</TextBlock>
                 <PasswordBox Margin="0,10" Password="123456" />
                 <Button Command="{Binding LoginCommand}" Content="Login" />
             </StackPanel>
         </Border>
     </Grid>
 </local:BasePage>
  1. 定义PageViewConverter

    csharp 复制代码
    public class PageViewConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            switch ((ApplicationPage)value)
            {
                case ApplicationPage.Empty:
                    return new TextBlock { Text = "404 Not Found" };
                case ApplicationPage.Login:
                    return new Login();
                case ApplicationPage.Home:
                    return new Home();
                default:
                    throw new ArgumentException("Invalid value passed to ApplicationPageViewConverter");
            }
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
  2. 完成MainWindow

xml 复制代码
 <Window ...>
     <Window.DataContext>
         <local:MainViewModel/>
     </Window.DataContext>
     <Window.Resources>
         <share:PageViewConverter x:Key="pageConv"/>
     </Window.Resources>
     <ContentControl Content="{Binding CurrentPage,Converter={StaticResource pageConv}}"/>
 </Window>

改进

  1. 可以结合依赖注入的方式来实现

  2. 导航方法可以封装为一个NavigationService服务

csharp 复制代码
 //封装服务
 class NavigationService
 {
     public static NavigationService Instance { get; } = new NavigationService();
 
     private MainViewModel mainVM;
 
     public void Navigate(ApplicationPage page)
     {
         if (mainVM == null)
         {
             mainVM = (MainViewModel)App.Current.MainWindow.DataContext;
         }
 
         mainVM.CurrentPage = page;
     }
 }
 //原来的方式
 void Logout()
 {
    var mainVM = App.Current.MainWindow.DataContext as MainViewModel;
    mainVM!.CurrentPage = ApplicationPage.Login;
 }
 //使用封装好的服务
 void Login()
 {
     NavigationService.Instance.Navigate(ApplicationPage.Login);
 }

四、使用Frame和NavigationService

实现上一章节功能,本质上是使用依赖注入的方式将View和ViewModel进行绑定,并利用Frame的自带的Navigate方法进行导航

  1. 定义ViewModel
csharp 复制代码
 public class ViewModelBase : ObservableObject{}
 
 public partial class MainWindowViewModel : ViewModelBase
 {
     private readonly NavigationService navigationService;
     //依赖注入
     public MainWindowViewModel(NavigationService navigationService)
     {
         this.navigationService = navigationService;
     }
 
     [RelayCommand]
     void Loaded()
     {  //navigationService实现的导航方法
         navigationService.Navigate<LoginViewModel>();
     }
 }
 
 public partial class HomeViewModel : ViewModelBase
 {
     [ObservableProperty]
     string? userName;
 }
 
 public partial class LoginViewModel : ViewModelBase
 {
     private readonly NavigationService navigationService;
     //依赖注入
     public string UserName { get; set; } = "Sergio";
 
     public LoginViewModel(NavigationService navigationService)
     {
         this.navigationService = navigationService;
     }
 
     [RelayCommand]
     void Login()
     {   //navigationService实现的导航方法,此处进行了传参
         navigationService.Navigate<HomeViewModel>(new Dictionary<string, object?>
         {
             [nameof(HomeViewModel.UserName)] = UserName
         });
     }
 }
  1. 定义View

主窗口,使用Behaviors实现mvvm模式

xml 复制代码
<Window
    xmlns:b="http://schemas.microsoft.com/xaml/behaviors">
    <b:Interaction.Triggers>
        <b:EventTrigger>
            <b:InvokeCommandAction Command="{Binding LoadedCommand}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>
</Window>

主窗口后台类

csharp 复制代码
public partial class MainWindow : Window
{
    public MainWindow(MainWindowViewModel viewModel,Frame frame)
    {
        InitializeComponent();
        DataContext = viewModel;
        AddChild(frame);
    }
}

其他View

xml 复制代码
<!--使用Page来承载内容-->
<Page ...>
    <Grid>
        <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   d:Text="Hello, world!"
                   Text="{Binding UserName, StringFormat='Hello, {0}!'}"
                   FontSize="32" />
    </Grid>
</Page>

<Page ...>
    <Grid>
        <Border Padding="10"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                BorderThickness="1"
                CornerRadius="10"
                BorderBrush="LightGray">
            <StackPanel Width="300">
                <TextBlock HorizontalAlignment="Center" FontSize="28">Login</TextBlock>
                <Separator Margin="0,10" />
                <TextBlock>User name:</TextBlock>
                <TextBox Margin="0,10" Text="{Binding UserName}" InputMethod.IsInputMethodEnabled="False" />
                <TextBlock>Password:</TextBlock>
                <PasswordBox Margin="0,10" Password="123456" />
                <Button Content="Login" Command="{Binding LoginCommand}" />
            </StackPanel>
        </Border>
    </Grid>
</Page>

在后台类中使用依赖注入的方式定义DataContext

csharp 复制代码
public Home(HomeViewModel viewModel)
{
    InitializeComponent();
    DataContext = viewModel;
}

public Login(LoginViewModel viewModel)
{
    InitializeComponent();
    DataContext = viewModel;
}
  1. 实现NavigationService
csharp 复制代码
 public class NavigationService
 {
     //注册了单例的Frame
     private readonly Frame? mainFrame;
 
     public NavigationService(Frame? frame)
     {
         mainFrame = frame;
         //要使用LoadCompleted事件
         mainFrame.LoadCompleted += MainFrame_LoadCompleted;
     }
 
     private void MainFrame_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
     {
         if (e.ExtraData is not Dictionary<string,object?> extraData)
         {
             return;
         }
         if ((mainFrame?.Content as FrameworkElement)?.DataContext is not ViewModelBase vm)
         {
             return;
         }
         foreach (var item in extraData)
         {
             //为每个属性赋值
             vm.GetType().GetProperty(item.Key)?.SetValue(vm, item.Value);
         }
     }
    //根据VM类型查找View,要注意VM和View的命名规范
     private Type? FindView<T>()
     {
         return Assembly.GetAssembly(typeof(T))?.GetTypes().FirstOrDefault(x => x.Name == typeof(T).Name.Replace("ViewModel", ""));
     }
     public void Navigate<T>(Dictionary<string,object?>? extraData=null) where T:ViewModelBase
     {
         var viewType = FindView<T>();
         if (viewType is null)
             return;
         var page = App.Current.Services.GetService(viewType) as Page;
 		//利用Frame的Navigate方法进行导航和传参
         mainFrame?.Navigate(page,extraData);
     }   
 }
  1. 注册需要的类,此案例在App.cs中进行注册
csharp 复制代码
 public partial class App : Application
 {
     public IServiceProvider Services { get; }
 
     public static new App Current => (App)Application.Current;
 
     public App()
     {
         var container = new ServiceCollection();
 
         container.AddSingleton(_ => new Frame { NavigationUIVisibility = NavigationUIVisibility.Hidden });
 
         container.AddSingleton<MainWindow>();
         container.AddSingleton<MainWindowViewModel>();
 
         container.AddTransient<Login>();
         container.AddTransient<Home>();
 
         container.AddTransient<LoginViewModel>();
         container.AddTransient<HomeViewModel>();
 
         container.AddSingleton<NavigationService>();
 
         Services = container.BuildServiceProvider();
     }
 
     protected override void OnStartup(StartupEventArgs e)
     {
         base.OnStartup(e);
 
         MainWindow = Services.GetRequiredService<MainWindow>();
         MainWindow.Show();
     }
 }
相关推荐
I_Am_Me_13 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
重生之我是数学王子23 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手25 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z29 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹36 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE37 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
zwjapple1 小时前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
前端每日三省1 小时前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang