MVVM – 层次结构和导航
构建 MVVM 应用程序时,您通常会将复杂的信息屏幕分解为一组父视图和子视图,其中子视图包含在面板或容器控件中的父视图中,并形成使用层次结构。
分解复杂的视图后,并不意味着您分离到其自己的 XAML 文件中的每一部分子内容都必须是 MVVM 视图。
内容块仅提供将某些内容呈现到屏幕上的结构,不支持用户对该内容的任何输入或操作。
它可能不需要单独的 ViewModel,但它可能只是一个基于父 ViewModel 公开的属性呈现的块 XAML。
最后,如果您有视图和 ViewModel 的层次结构,则父 ViewModel 可以成为通信中心,以便每个子 ViewModel 可以与其他子 ViewModel 保持分离ViewModel 并尽可能多地从其父级获取。
让我们看一个例子,我们将在其中定义不同视图之间的简单层次结构。创建一个新的 WPF 应用程序项目 MVVMHierarchiesDemo
步骤 1 − 将三个文件夹(Model、ViewModel 和 Views)添加到您的项目中。
步骤 2 −在 Model 文件夹中添加 Customer 和 Order 类,在 Views 文件夹中添加 CustomerListView 和 OrderView,在 ViewModel 文件夹中添加 CustomerListViewModel 和 OrderViewModel,如下图所示。
步骤 3 − 在 CustomerListView 和 OrderView 中添加文本块。以下是 CustomerListView.xaml 文件。
<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView" xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d = "http://schemas.microsoft.com/expression/blend/2008" xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300"> <Grid> <TextBlock Text = "Customer List View"/> </Grid> </UserControl>
以下是 OrderView.xaml 文件。
<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView" xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d ="http://schemas.microsoft.com/expression/blend/2008" xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300"> <Grid> <TextBlock Text = "Order View"/> </Grid> </UserControl>
现在我们需要一些东西来托管这些视图,并且在我们的 MainWindow 中为它们提供一个好位置,因为它是一个简单的应用程序。我们需要一个容器控件,我们可以在其中放置我们的视图并以导航方式切换它们。为此,我们需要在 MainWindow.xaml 文件中添加 ContentControl,我们将使用它的内容属性并将其绑定到 ViewModel 引用。
现在在资源字典中为每个视图定义数据模板。以下是 MainWindow.xaml 文件。请注意每个数据模板如何将数据类型(ViewModel 类型)映射到相应的视图。
<Window x:Class = "MVVMHierarchiesDemo.MainWindow" xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d = "http://schemas.microsoft.com/expression/blend/2008" xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local = "clr-namespace:MVVMHierarchiesDemo" xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views" xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel" mc:Ignorable = "d" Title = "MainWindow" Height = "350" Width = "525"> <Window.DataContext> <local:MainWindowViewModel/> </Window.DataContext> <Window.Resources> <DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}"> <views:CustomerListView/> </DataTemplate> <DataTemplate DataType = "{x:Type viewModels:OrderViewModel}"> <views:OrderView/> </DataTemplate> </Window.Resources> <Grid> <ContentControl Content = "{Binding CurrentView}"/> </Grid> </Window>
只要当前视图模型设置为 CustomerListViewModel 的实例,它就会呈现一个挂接了 ViewModel 的 CustomerListView。这是一个订单 ViewModel,它会呈现 OrderView 等等。
我们现在需要一个具有 CurrentViewModel 属性的 ViewModel,以及一些逻辑和命令,以便能够在属性内切换 ViewModel 的当前引用。
让我们为这个 MainWindow 创建一个名为 MainWindowViewModel 的 ViewModel。我们可以从 XAML 创建 ViewModel 的实例,并使用它来设置窗口的 DataContext 属性。为此,我们需要创建一个基类来封装 ViewModel 的 INotifyPropertyChanged 实现。
此类背后的主要思想是封装 INotifyPropertyChanged 实现并为派生类提供帮助方法,以便它们可以轻松触发适当的通知。以下是 BindableBase 类的实现。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo { class BindableBase : INotifyPropertyChanged { protected virtual void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null) { if (object.Equals(member, val)) return; member = val; PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged = delegate { }; } }
现在是时候使用我们的 CurrentViewModel 属性开始进行一些视图切换了。我们只需要某种方法来驱动此属性的设置。我们将这样做,以便最终用户可以命令转到客户列表或订单视图。首先在项目中添加一个将实现 ICommand 接口的新类。以下是 ICommand 接口的实现。
using System; using System.Windows.Input; namespace MVVMHierarchiesDemo { public class MyICommand<T> : ICommand { Action<T> _TargetExecuteMethod; Func<T, bool> _TargetCanExecuteMethod; public MyICommand(Action<T> executeMethod) { _TargetExecuteMethod = executeMethod; } public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) { _TargetExecuteMethod = executeMethod; _TargetCanExecuteMethod = canExecuteMethod; } public void RaiseCanExecuteChanged() { CanExecuteChanged(this, EventArgs.Empty); } #region ICommand Members bool ICommand.CanExecute(object parameter) { if (_TargetCanExecuteMethod != null) { T tparm = (T)parameter; return _TargetCanExecuteMethod(tparm); } if (_TargetExecuteMethod != null) { return true; } return false; } // Beware - should use weak references if command instance lifetime is longer than lifetime of UI objects that get hooked up to command // Prism commands solve this in their implementation public event EventHandler CanExecuteChanged = delegate { }; void ICommand.Execute(object parameter) { if (_TargetExecuteMethod != null) { _TargetExecuteMethod((T)parameter); } } #endregion } }
我们现在需要为这些 ViewModel 设置一些顶级导航,并且切换逻辑应属于 MainWindowViewModel 内部。为此,我们将使用一个名为 on navigation 的方法,该方法接受字符串目标并返回 CurrentViewModel 属性。
private void OnNav(string destination) { switch (destination) { case "orders": CurrentViewModel = orderViewModelModel; break; case "customers": default: CurrentViewModel = custListViewModel; break; } }
为了导航这些不同的视图,我们需要在 MainWindow.xaml 文件中添加两个按钮。以下是完整的 XAML 文件实现。
<Window x:Class = "MVVMHierarchiesDemo.MainWindow" xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d = "http://schemas.microsoft.com/expression/blend/2008" xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local = "clr-namespace:MVVMHierarchiesDemo" xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views" xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel" mc:Ignorable = "d" Title = "MainWindow" Height = "350" Width = "525"> <Window.DataContext> <local:MainWindowViewModel/> </Window.DataContext> <Window.Resources> <DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}"> <views:CustomerListView/> </DataTemplate> <DataTemplate DataType = "{x:Type viewModels:OrderViewModel}"> <views:OrderView/> </DataTemplate> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height = "Auto" /> <RowDefinition Height = "*" /> </Grid.RowDefinitions> <Grid x:Name = "NavBar"> <Grid.ColumnDefinitions> <ColumnDefinition Width = "*" /> <ColumnDefinition Width = "*" /> <ColumnDefinition Width = "*" /> </Grid.ColumnDefinitions> <Button Content = "Customers" Command = "{Binding NavCommand}" CommandParameter = "customers" Grid.Column = "0" /> <Button Content = "Order" Command = "{Binding NavCommand}" CommandParameter = "orders" Grid.Column = "2" /> </Grid> <Grid x:Name = "MainContent" Grid.Row = "1"> <ContentControl Content = "{Binding CurrentViewModel}" /> </Grid> </Grid> </Window>
以下是完整的 MainWindowViewModel 实现。
using MVVMHierarchiesDemo.ViewModel; using MVVMHierarchiesDemo.Views; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo { class MainWindowViewModel : BindableBase { public MainWindowViewModel() { NavCommand = new MyICommand<string>(OnNav); } private CustomerListViewModel custListViewModel = new CustomerListViewModel(); private OrderViewModel orderViewModelModel = new OrderViewModel(); private BindableBase _CurrentViewModel; public BindableBase CurrentViewModel { get {return _CurrentViewModel;} set {SetProperty(ref _CurrentViewModel, value);} } public MyICommand<string> NavCommand { get; private set; } private void OnNav(string destination) { switch (destination) { case "orders": CurrentViewModel = orderViewModelModel; break; case "customers": default: CurrentViewModel = custListViewModel; break; } } } }
从 BindableBase 类派生所有 ViewModel。编译并执行上述代码后,您将看到以下输出。
如您所见,我们在 MainWindow 上仅添加了两个按钮和一个 CurrentViewModel。如果您单击任何按钮,它将导航到该特定视图。让我们单击 Customers 按钮,您将看到显示 CustomerListView。
为了更好地理解,我们建议您逐步执行上述示例。