MVVM – 层次结构和导航


在构建 MVVM 应用程序时,您通常会将复杂的信息屏幕分解为一组父视图和子视图,其中子视图包含在面板或容器控件的父视图中,并形成使用本身的层次结构。

  • 分解复杂的视图后,并不意味着您分离到自己的 XAML 文件中的每一个子内容都必须是 MVVM 视图。

  • 内容块仅提供将某些内容呈现到屏幕上的结构,并且不支持用户对该内容的任何输入或操作。

  • 它可能不需要单独的 ViewModel,但它可能只是一个基于父 ViewModel 公开的属性进行渲染的块 XAML。

  • 最后,如果您有 View 和 ViewModel 的层次结构,则父 ViewModel 可以成为通信中心,以便每个子 ViewModel 可以尽可能保持与其他子 ViewModel 及其父视图的解耦。

MVVM 层次结构和导航

让我们看一个示例,在该示例中我们将在不同视图之间定义一个简单的层次结构。创建一个新的WPF应用程序项目MVVMHierarchiesDemo

WPF应用程序

步骤 1 - 将三个文件夹(Model、ViewModel 和 Views)添加到您的项目中。

WPF应用步骤1

步骤 2 - 在 Model 文件夹中添加 Customer 和 Order 类,在 Views 文件夹中添加 CustomerListView 和 OrderView,在 ViewModel 文件夹中添加 CustomerListViewModel 和 OrderViewModel,如下图所示。

WPF应用Step2

步骤 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.xaml 文件中添加 ContentControl,我们将使用其 content 属性并将其绑定到 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 内部。为此,我们将使用在导航上调用的方法,该方法接受字符串目标并返回 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。当上面的代码被编译并执行时,您将看到以下输出。

层次结构和导航 MainWindow1

正如您所看到的,我们在主窗口上仅添加了两个按钮和一个 CurrentViewModel。如果您单击任何按钮,它将导航到该特定视图。让我们单击“Customers”按钮,您将看到显示了“CustomerListView”。

层次结构和导航 MainWindow2

我们建议您逐步执行上述示例,以便更好地理解。