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();
     }
 }
相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa10 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio