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();
     }
 }
相关推荐
qq_5375626720 小时前
跨语言调用C++接口
开发语言·c++·算法
wjs202421 小时前
DOM CDATA
开发语言
Tingjct21 小时前
【初阶数据结构-二叉树】
c语言·开发语言·数据结构·算法
猷咪21 小时前
C++基础
开发语言·c++
IT·小灰灰21 小时前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧21 小时前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q21 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳021 小时前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾21 小时前
php 对接deepseek
android·开发语言·php
2601_9498683621 小时前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter