MVVM - 快速指南


MVVM – 简介

组织代码的有序且可能是最可重用的方法是使用“MVVM”模式。模型、视图、ViewModel(MVVM 模式)旨在指导您如何组织和构建代码以编写可维护、可测试和可扩展的应用程序。

模型- 它只是保存数据,与任何业务逻辑无关。

ViewModel - 它充当模型和视图之间的链接/连接,使东西看起来很漂亮。

视图- 它只是保存格式化数据,本质上将所有内容委托给模型。

查看模型

分开演示

为了避免将应用程序逻辑放入代码隐藏或 XAML 中而导致的问题,最好使用称为分离表示的技术。我们试图避免这种情况,我们将使用 XAML 和代码隐藏来直接处理用户界面对象所需的最少内容。用户界面类还包含复杂交互Behave、应用程序逻辑以及其他所有内容的代码,如下图左侧所示。

分开演示
  • 通过分离的呈现,用户界面类变得更加简单。当然,它有 XAML,但背后的代码只做实用的事情。

  • 应用程序逻辑属于一个单独的类,通常称为模型。

  • 然而,这并不是故事的全部。如果您停在这里,您可能会重复一个非常常见的错误,这将导致您走上数据绑定疯狂之路。

  • 许多开发人员尝试使用数据绑定将 XAML 中的元素直接连接到模型中的属性。

  • 有时这可能没问题,但通常情况并非如此。问题是模型完全关心应用程序做什么,而不关心用户如何与应用程序交互。

  • 呈现数据的方式通常与数据的内部结构方式有些不同。

  • 此外,大多数用户界面都有一些不属于应用程序模型的状态。

  • 例如,如果您的用户界面使用拖放操作,则需要跟踪诸如被拖动的项目现在所在的位置、当其在可能的放置目标上移动时其外观应如何变化以及这些放置目标可能如何变化等信息。当项目被拖到它们上面时会发生变化。

  • 这种状态可能会变得异常复杂,并且需要进行彻底的测试。

  • 在实践中,您通常需要在用户界面和模型之间放置一些其他类。这有两个重要作用。

    • 首先,它使您的应用程序模型适应特定的用户界面视图。

    • 其次,它是任何重要交互逻辑所在的地方,我的意思是让用户界面按照您想要的方式运行所需的代码。

MVVM – 优点

MVVM 模式最终是 MVC 模式的现代结构,因此主要目标仍然是相同的,即在领域逻辑和表示层之间提供清晰的分离。以下是 MVVM 模式的一些优点和缺点。

关键的好处是允许视图和模型之间真正分离,而不仅仅是实现分离以及由此获得的效率。这实际上意味着,当您的模型需要更改时,可以轻松更改它,而无需视图,反之亦然。

应用 MVVM 会产生以下三个重要的关键内容。

可维护性

  • 不同类型的代码的清晰分离应该可以更容易地进入一个或多个更细粒度和更集中的部分,并且无需担心即可进行更改。

  • 这意味着您可以保持敏捷并继续快速迁移到新版本。

可测试性

  • 使用 MVVM,每段代码都更加精细,如果正确实现,您的外部和内部依赖项将位于与您想要测试的核心逻辑部分不同的代码段中。

  • 这使得针对核心逻辑编写单元测试变得更加容易。

  • 确保它在编写时能够正常工作,并且即使在维护过程中情况发生变化时也能继续工作。

可扩展性

  • 由于清晰的分离边界和更细粒度的代码片段,它有时与可维护性重叠。

  • 您有更好的机会使这些部件更加可重复使用。

  • 它还能够将执行类似操作的新代码替换或添加到架构中的正确位置。

MVVM 模式的明显目的是抽象视图,从而减少代码隐藏中的业务逻辑量。然而,以下是其他一些坚实的优势 -

  • ViewModel 比代码隐藏或事件驱动代码更容易进行单元测试。
  • 您可以在没有尴尬的 UI 自动化和交互的情况下对其进行测试。
  • 表示层和逻辑是松散耦合的。

缺点

  • 有些人认为对于简单的 UI,MVVM 可能有点大材小用。
  • 同样,在更大的情况下,设计 ViewModel 可能会很困难。
  • 当我们有复杂的数据绑定时,调试会有点困难。

MVVM——职责

MVVM 模式由三部分组成:模型、视图和视图模型。大多数开发人员一开始都不太清楚模型、视图和视图模型应该包含什么、不应该包含什么以及每个部分的职责是什么。

在本章中我们将学习 MVVM 模式中各个部分的职责,以便您可以清楚地了解什么样的代码在哪里。MVVM 实际上是客户端的分层架构,如下图所示。

MVVM 职责
  • 表示层由视图组成。

  • 逻辑层是视图模型。

  • 表示层是模型对象的组合。

  • 生成并保留它们的客户端服务要么在两层应用程序中进行定向访问,要么通过服务调用然后再调用您的应用程序。

  • 客户端服务并不是 MVVM 模式的正式组成部分,但它通常与 MVVM 一起使用以实现进一步的分离并避免重复代码。

模型责任

一般来说,模型是最容易理解的。它是支持应用程序中的视图的客户端数据模型。

  • 它由具有属性的对象和一些变量组成,以在内存中包含数据。

  • 其中一些属性可能会引用其他模型对象并创建对象图,该对象图作为一个整体是模型对象。

  • 模型对象应该引发属性更改通知,这在 WPF 中意味着数据绑定。

  • 最后一个责任是验证,这是可选的,但您可以通过 INotifyDataErrorInfo/IDataErrorInfo 等接口使用 WPF 数据绑定验证功能,将验证信息嵌入到模型对象上

查看职责

视图的主要目的和职责是定义用户在屏幕上看到的内容的结构。该结构可以包含静态和动态部分。

  • 静态部件是定义视图组成的控件和控件布局的 XAML 层次结构。

  • 动态部分就像定义为视图一部分的动画或状态更改。

  • MVVM 的主要目标是视图中不应该有任何代码隐藏。

  • 视图中不可能没有后面的代码。鉴于您至少需要构造函数和调用来初始化组件。

  • 这个想法是事件处理、操作和数据操作逻辑代码不应该位于视图的后面代码中。

  • 还有其他类型的代码必须放在任何需要引用 UI 元素的代码后面的代码中,这些代码本质上是视图代码。

ViewModel 职责

  • ViewModel是MVVM应用的要点。ViewModel 的主要职责是向视图提供数据,以便视图可以将该数据显示在屏幕上。

  • 它还允许用户与数据交互并更改数据。

  • ViewModel 的另一个关键职责是封装视图的交互逻辑,但这并不意味着应用程序的所有逻辑都应该放入 ViewModel 中。

  • 它应该能够处理适当的调用顺序,以便根据用户或视图上的任何更改发生正确的事情。

  • ViewModel 还应该管理任何导航逻辑,例如决定何时导航到不同的视图。

MVVM——第一个应用程序

在本章中,我们将学习如何使用 MVVM 模式进行简单的输入屏幕以及您可能已经习惯的 WPF 应用程序。

让我们看一个使用 MVVM 方法的简单示例。

步骤 1 - 创建一个新的 WPF 应用程序项目 MVVMDemo。

首次申请步骤1

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

首次申请第2步

步骤 3 - 在模型文件夹中添加一个 StudentModel 类,并将以下代码粘贴到该类中

using System.ComponentModel;

namespace MVVMDemo.Model {
 
   public class StudentModel {}
	
   public class Student : INotifyPropertyChanged {
      private string firstName; 
      private string lastName;
		
      public string FirstName { 
         get { 
            return firstName; 
         }
			
         set { 
            if (firstName != value) { 
               firstName = value; 
               RaisePropertyChanged("FirstName"); 
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
		
      public string LastName { 
         get {return lastName; } 
			
         set {
            if (lastName != value) { 
               lastName = value;
               RaisePropertyChanged("LastName");
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
		
      public string FullName { 
         get { 
            return firstName + " " + lastName; 
         } 
      }
		
      public event PropertyChangedEventHandler PropertyChanged;
		
      private void RaisePropertyChanged(string property) {
         if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
         } 
      } 
   } 
}

步骤 4 - 将另一个 StudentViewModel 类添加到 ViewModel 文件夹中并粘贴以下代码。

using MVVMDemo.Model; 
using System.Collections.ObjectModel;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel { 
	
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
				
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      } 
   } 
}

步骤 5 - 通过右键单击“视图”文件夹并选择“添加”>“新项目...”来添加新的用户控件 (WPF)

首次申请第5步

步骤 6 - 单击添加按钮。现在您将看到 XAML 文件。将以下代码添加到包含不同 UI 元素的 StudentView.xaml 文件中。

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   mc:Ignorable = "d" 
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
		
         <ItemsControl ItemsSource = "{Binding Path = Students}">
			
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
                  <StackPanel Orientation = "Horizontal">
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/>
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/>
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/>
								
                  </StackPanel> 
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
				
         </ItemsControl> 
			
      </StackPanel> 
   </Grid> 
	
</UserControl>

步骤 7 - 现在使用以下代码将 StudentView 添加到 MainPage.xaml 文件中。

<Window x:Class = "MVVMDemo.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:MVVMDemo"
   xmlns:views = "clr-namespace:MVVMDemo.Views"
   mc:Ignorable = "d"
   Title = "MainWindow" Height = "350" Width = "525">
	
   <Grid>
      <views:StudentView x:Name = "StudentViewControl" Loaded = "StudentViewControl_Loaded"/>
   </Grid>
	
</Window>

步骤 8 - 这是 MainPage.xaml.cs 文件中 Loaded 事件的实现,它将从 ViewModel 更新视图。

using System.Windows;

namespace MVVMDemo {

   /// <summary>
      /// Interaction logic for MainWindow.xaml
   /// </summary>
	
   public partial class MainWindow : Window {
	
      public MainWindow() {
         InitializeComponent();
      }
		
      private void StudentViewControl_Loaded(object sender, RoutedEventArgs e) {
         MVVMDemo.ViewModel.StudentViewModel studentViewModelObject = 
            new MVVMDemo.ViewModel.StudentViewModel();
         studentViewModelObject.LoadStudents();
			
         StudentViewControl.DataContext = studentViewModelObject;
      }
   }
}

步骤 9 - 编译并执行上述代码后,您将在主窗口上收到以下输出。

首次申请第9步

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

MVVM – 连接视图

在本章中,我们将介绍将视图连接到 ViewModel 的不同方法。首先,让我们看一下 View First 构造,我们可以在 XAML 中声明它。正如我们在上一章中看到的示例,我们从主窗口连接了一个视图。现在我们将看到连接视图的其他方法。

我们也将在本章中使用相同的示例。以下是相同的模型类实现。

using System.ComponentModel;

namespace MVVMDemo.Model {
 
   public class StudentModel {} 
	
   public class Student : INotifyPropertyChanged { 
      private string firstName; 
      private string lastName;
		
      public string FirstName { 
         get { return firstName; }
			
         set { 
            if (firstName != value) { 
               firstName = value; 
               RaisePropertyChanged("FirstName");
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
	
      public string LastName {
         get { return lastName; } 
			
         set { 
            if (lastName != value) { 
               lastName = value; 
               RaisePropertyChanged("LastName");
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
	
      public string FullName { 
         get { 
            return firstName + " " + lastName; 
         } 
      }
	
      public event PropertyChangedEventHandler PropertyChanged;
	
      private void RaisePropertyChanged(string property) { 
         if (PropertyChanged != null) { 
            PropertyChanged(this, new PropertyChangedEventArgs(property)); 
         } 
      } 
   }  
}

这是 ViewModel 类的实现。这次在默认构造函数中调用了 LoadStudents 方法。

using MVVMDemo.Model; 
using System.Collections.ObjectModel;

namespace MVVMDemo.ViewModel{ 

   public class StudentViewModel { 
	
      public StudentViewModel() { 
         LoadStudents(); 
      } 
		
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
			
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      } 
   } 
}

无论视图是窗口、用户控件还是页面,解析器通常从上到下、从左到右工作。它在遇到每个元素时调用默认构造函数。有两种构建视图的方法。您可以对它们使用任何一个。

  • 在 XAML 中查看第一个构造
  • 查看代码隐藏中的第一个构造

在 XAML 中查看第一个构造

一种方法是简单地将 ViewModel 添加为 DataContext 属性的 setter 中的嵌套元素,如以下代码所示。

<UserControl.DataContext> 
   <viewModel:StudentViewModel/> 
</UserControl.DataContext>

这是完整的 View XAML 文件。

<UserControl x:Class="MVVMDemo.Views.StudentView"
   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:MVVMDemo.Views"
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
   mc:Ignorable = "d"
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.DataContext>
      <viewModel:StudentViewModel/>
   </UserControl.DataContext>
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}">
			
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"> 
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                      <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/>
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate>
				
         </ItemsControl> 
      </StackPanel> 
   </Grid> 
	
</UserControl>

查看代码隐藏中的第一个构造

另一种方法是,您可以通过在视图后面的代码中通过实例设置 DataContext 属性来自己构建视图模型,从而获得视图优先构造。

通常,DataContext 属性是在视图的构造函数方法中设置的,但您也可以将构造推迟到触发视图的 Load 事件。

using System.Windows.Controls;

namespace MVVMDemo.Views {
 
   /// <summary> 
      /// Interaction logic for StudentView.xaml 
   /// </summary> 
	
   public partial class StudentView : UserControl { 
      public StudentView() { 
         InitializeComponent(); 
         this.DataContext = new MVVMDemo.ViewModel.StudentViewModel(); 
      } 
   } 
}

在代码隐藏而不是 XAML 中构造视图模型的原因之一是视图模型构造函数需要参数,但 XAML 解析只能构造在默认构造函数中定义的元素。

现在,在这种情况下,View 的 XAML 文件将如下代码所示。

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   mc:Ignorable = "d" 
   d:DesignHeight = "300" 
   d:DesignWidth = "300">
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}"> 
			
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"<
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/> 
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate>
				
         </ItemsControl> 
      </StackPanel> 
   </Grid>
	
</UserControl>

您可以在 MainWindow 中声明此视图,如 MainWindow.XAML 文件中所示。

<Window x:Class = "MVVMDemo.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:MVVMDemo" 
   xmlns:views = "clr-namespace:MVVMDemo.Views" 
   mc:Ignorable = "d" 
   Title = "MainWindow" Height = "350" Width = "525"> 
	
   <Grid> 
      <views:StudentView x:Name = "StudentViewControl"/> 
   </Grid> 
	
</Window>

编译并执行上述代码后,您将在主窗口上看到以下输出。

连接视图主窗口

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

MVVM – 连接 ViewModel

在本章中,我们将介绍如何连接 ViewModel。这是上一章的延续,我们在上一章中讨论了视图优先结构。现在,第一个构造的下一个形式是元模式,称为ViewModelLocator。它是一个伪模式,位于 MVVM 模式之上。

  • 在 MVVM 中,每个视图都需要连接到它的 ViewModel。

  • ViewModelLocator 是一种集中代码并进一步解耦视图的简单方法。

  • 这意味着它不必明确了解 ViewModel 类型以及如何构造它。

  • 使用 ViewModelLocator 有多种不同的方法,但这里我们使用与 PRISM 框架的一部分最相似的方法。

ViewModelLocator 提供了一种标准的、一致的、声明性的和松散耦合的方式来进行视图优先构建,从而自动执行将 ViewModel 连接到视图的过程。下图展示了ViewModelLocator的高层流程。

连接 ViewModel

步骤 1 - 找出正在构建的视图类型。

步骤 2 - 识别该特定视图类型的视图模型。

步骤 3 - 构建 ViewModel。

步骤 4 - 将视图 DataContext 设置为 ViewModel。

为了理解基本概念,让我们继续上一章的相同示例来看看 ViewModelLocator 的简单示例。如果您查看 StudentView.xaml 文件,您将看到我们已静态连接 ViewModel。

现在如以下程序所示,注释这些 XAML 代码也会从代码隐藏中删除代码。

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
	
   <!--<UserControl.DataContext> 
      <viewModel:StudentViewModel/> 
   </UserControl.DataContext>-->
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}">
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal">
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/> 
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
         </ItemsControl> 
      </StackPanel> 
   </Grid>
	
</UserControl>

现在,让我们创建一个新文件夹 VML 并添加一个新的公共类 ViewModelLocator,它将包含一个附加属性(依赖属性)AutoHookedUpViewModel,如以下代码所示。

public static bool GetAutoHookedUpViewModel(DependencyObject obj) { 
   return (bool)obj.GetValue(AutoHookedUpViewModelProperty); 
}

public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) { 
   obj.SetValue(AutoHookedUpViewModelProperty, value); 
}

// Using a DependencyProperty as the backing store for AutoHookedUpViewModel. 
//This enables animation, styling, binding, etc...
 
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
   DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
   typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false,
   AutoHookedUpViewModelChanged));

现在您可以看到基本的附加属性定义。要向属性添加Behave,我们需要为此属性添加更改的事件处理程序,其中包含为 View 连接 ViewModel 的自动过程。执行此操作的代码如下 -

private static void AutoHookedUpViewModelChanged(DependencyObject d, 
   DependencyPropertyChangedEventArgs e) { 
   if (DesignerProperties.GetIsInDesignMode(d)) return; 
   var viewType = d.GetType(); 
   string str = viewType.FullName; 
   str = str.Replace(".Views.", ".ViewModel."); 
	
   var viewTypeName = str; 
   var viewModelTypeName = viewTypeName + "Model"; 
   var viewModelType = Type.GetType(viewModelTypeName); 
   var viewModel = Activator.CreateInstance(viewModelType);
   ((FrameworkElement)d).DataContext = viewModel; 
}

以下是 ViewModelLocator 类的完整实现。

using System; 
using System.ComponentModel; 
using System.Windows;

namespace MVVMDemo.VML { 

   public static class ViewModelLocator { 
	
      public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
         return (bool)obj.GetValue(AutoHookedUpViewModelProperty); 
      }
		
      public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) { 
         obj.SetValue(AutoHookedUpViewModelProperty, value); 
      }
		
      // Using a DependencyProperty as the backing store for AutoHookedUpViewModel. 
		
      //This enables animation, styling, binding, etc...
      public static readonly DependencyProperty AutoHookedUpViewModelProperty =
         DependencyProperty.RegisterAttached("AutoHookedUpViewModel", 
         typeof(bool), typeof(ViewModelLocator), new
         PropertyMetadata(false, AutoHookedUpViewModelChanged));
		
      private static void AutoHookedUpViewModelChanged(DependencyObject d,
         DependencyPropertyChangedEventArgs e) { 
         if (DesignerProperties.GetIsInDesignMode(d)) return; 
         var viewType = d.GetType(); 
			
         string str = viewType.FullName; 
         str = str.Replace(".Views.", ".ViewModel."); 
			
         var viewTypeName = str; 
         var viewModelTypeName = viewTypeName + "Model";
         var viewModelType = Type.GetType(viewModelTypeName); 
         var viewModel = Activator.CreateInstance(viewModelType);
			
        ((FrameworkElement)d).DataContext = viewModel; 
      } 
   } 
}

首先要做的是添加一个命名空间,以便我们可以访问项目根目录中的 ViewModelLocator 类型。然后在视图类型的路由元素上,添加 AutoHookedUpViewModel 属性并将其设置为 true。

xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"

以下是 StudentView.xaml 文件的完整实现。

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
   
   <!--<UserControl.DataContext> 
      <viewModel:StudentViewModel/> 
   </UserControl.DataContext>-->

   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}"> 
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"> 
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/>
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
         </ItemsControl> 
      </StackPanel>
   </Grid> 
	
</UserControl>

当上面的代码被编译并执行时,您将看到 ViewModelLocator 正在为该特定 View 连接 ViewModel。

连接 ViewModel 主窗口

需要注意的一个关键点是视图不再与其 ViewModel 的类型或构造方式耦合。这些都已移至 ViewModelLocator 内的中心位置。

MVVM – WPF 数据绑定

在本章中,我们将学习数据绑定如何支持 MVVM 模式。数据绑定是 MVVM 区别于其他 UI 分离模式(如 MVC 和 MVP)的关键功能。

  • 对于数据绑定,您需要构造一个视图或一组 UI 元素,然后需要绑定将指向的其他对象。

  • 视图中的 UI 元素绑定到 ViewModel 公开的属性。

  • View 和 ViewModel 的构建顺序取决于具体情况,因为我们首先介绍了 View。

  • 构造 View 和 ViewModel,并将 View 的 DataContext 设置为 ViewModel。

  • 绑定可以是 OneWay 或 TwoWay 数据绑定,以便在 View 和 ViewModel 之间来回流动数据。

让我们看一下同一示例中的数据绑定。下面是StudentView的XAML代码。

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">

   <!--<UserControl.DataContext> 
      <viewModel:StudentViewModel/> 
   </UserControl.DataContext>--> 

   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}"> 
            <ItemsControl.ItemTemplate>
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"> 
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/>
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/> 
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
         </ItemsControl> 
      </StackPanel> 
   </Grid> 

</UserControl>
  • 如果您查看上面的 XAML 代码,您将看到 ItemsControl 绑定到 ViewModel 公开的 Students 集合。

  • 您还可以看到 Student 模型的属性也有自己单独的绑定,并且这些绑定到 Textboxes 和 TextBlock。

  • ItemsControl 的 ItemSource 能够绑定到 Students 属性,因为视图的整体 DataContext 设置为 ViewModel。

  • 这里属性的单独绑定也是 DataContext 绑定,但由于 ItemSource 的工作方式,它们并不针对 ViewModel 本身进行绑定。

  • 当项目源绑定到其集合时,它会在呈现时为每个项目呈现一个容器,并将该容器的 DataContext 设置为该项目。因此,行中每个文本框和文本块的整体 DataContext 将是集合中的单个 Student。您还可以看到,TextBox 的这些绑定是 TwoWay 数据绑定,而 TextBlock 的这些绑定是 OneWay 数据绑定,因为您无法编辑 TextBlock。

当您再次运行该应用程序时,您将看到以下输出。

WPF 数据绑定主窗口

现在让我们将第一行第二个文本框中的文本从 Allin 更改为 Upston,然后按 Tab 键失去焦点。您将看到 TextBlock 文本也已更新。

更新的文本块

这是因为 TextBox 的绑定设置为 TwoWay 并且它也会更新模型,并且再次从模型更新 TextBlock。

MVVM – WPF 数据模板

模板描述了控件的整体外观和视觉外观。对于每个控件,都有一个与其关联的默认模板,该模板为该控件提供外观。在 WPF 应用程序中,当您想要自定义控件的视觉Behave和视觉外观时,可以轻松创建自己的模板。逻辑和模板之间的连接可以通过数据绑定来实现。

在 MVVM 中,还有另一种主要形式,称为 ViewModel 优先构造。

  • ViewModel 第一种构造方法利用 WPF 中隐式数据模板的功能。

  • 隐式数据模板可以自动从当前资源字典中为使用数据绑定的元素选择合适的模板。他们根据数据绑定呈现的数据对象的类型来执行此操作。首先,您需要有一些绑定到数据对象的元素。

让我们再次看一下我们的简单示例,您将了解如何首先利用数据模板(特别是隐式数据模板)来执行视图模型。这是我们的 StudentViewModel 类的实现。

using MVVMDemo.Model; 
using System.Collections.ObjectModel;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel {
	
      public StudentViewModel() { 
         LoadStudents(); 
      } 
		
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
			
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      } 
   } 
}

可以看到上面的ViewModel没有变化。我们将继续使用上一章中的相同示例。此 ViewModel 类仅公开 Students 集合属性并在构造时填充它。让我们转到 StudentView.xaml 文件,删除现有实现并在资源部分定义数据模板。

<UserControl.Resources> 
   <DataTemplate x:Key = "studentsTemplate">
	
      <StackPanel Orientation = "Horizontal"> 
         <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
            Width = "100" Margin = "3 5 3 5"/> 
				
         <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
            Width = "100" Margin = "0 5 3 5"/> 
				
         <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
            Margin = "0 5 3 5"/> 
      </StackPanel> 
		
   </DataTemplate> 
</UserControl.Resources>

现在添加一个列表框并将该列表框数据绑定到 Students 属性,如以下代码所示。

<ListBox ItemsSource = "{Binding Students}" ItemTemplate = "{StaticResource studentsTemplate}"/>

在资源部分中,DataTemplate 有一个 StudentsTemplate 键,然后要实际使用该模板,我们需要使用 ListBox 的 ItemTemplate 属性。现在您可以看到我们指示列表框使用该特定模板来渲染这些学生。以下是 StudentView.xaml 文件的完整实现。

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.Resources> 
      <DataTemplate x:Key = "studentsTemplate"> 
		
         <StackPanel Orientation = "Horizontal"> 
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
					
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>
	
   <Grid> 
      <ListBox 
         ItemsSource = "{Binding Students}" 
         ItemTemplate = "{StaticResource studentsTemplate}"/> 
   </Grid>
	
</UserControl>

当上面的代码编译并执行时,您将看到以下窗口,其中包含一个ListBox。每个 ListBoxItem 包含显示在 TextBlock 和文本框中的 Student 类对象数据。

WPF 数据模板主窗口

为了使其成为隐式模板,我们需要从列表框中删除 ItemTemplate 属性,并在模板定义中添加 DataType 属性,如以下代码所示。

<UserControl.Resources> 
   <DataTemplate DataType = "{x:Type data:Student}">
	
      <StackPanel Orientation = "Horizontal"> 
         <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
            Width = "100" Margin = "3 5 3 5"/> 
				
         <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
            Width = "100" Margin = "0 5 3 5"/> 
				
         <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
            Margin = "0 5 3 5"/> 
				
      </StackPanel> 
		
   </DataTemplate> 
</UserControl.Resources>
 
<Grid> 
   <ListBox ItemsSource = "{Binding Students}"/> 
</Grid>

在DataTemplate中,x:Type标记扩展非常重要,它就像XAML中的类型运算符。因此,基本上我们需要指向 MVVMDemo.Model 命名空间中的 Student 数据类型。以下是更新后的完整 XAML 文件。

<UserControl x:Class="MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:data = "clr-namespace:MVVMDemo.Model" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.Resources> 
      <DataTemplate DataType = "{x:Type data:Student}"> 
		
         <StackPanel Orientation = "Horizontal"> 
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
					
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>
	
   <Grid>
      <ListBox ItemsSource = "{Binding Students}"/> 
   </Grid> 
	
</UserControl>

当您再次运行此应用程序时,您仍然会获得具有数据模板的 Students 的相同呈现,因为它会通过查找适当的 DataTemplate 自动映射正在呈现的对象的类型。

数据模板

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

MVVM – 视图/视图模型通信

在本章中,我们将学习如何向 MVVM 应用程序添加交互性以及如何干净地调用逻辑。您还将看到,所有这一切都是通过维护松散耦合和良好的结构来完成的,这是 MVVM 模式的核心。为了理解这一切,首先让我们了解命令。

通过命令查看/视图模型通信

命令模式已经有很好的文档记录,并且几十年来经常使用设计模式。在此模式中有两个主要参与者:调用者和接收者。

视图和视图模型通信

祈求者

  • 调用者是一段可以执行一些命令式逻辑的代码。

  • 通常,它是用户在 UI 框架上下文中与之交互的 UI 元素。

  • 它可能只是应用程序中其他地方的另一块逻辑代码。

接收者

  • 接收器是调用程序触发时要执行的逻辑。

  • 在 MVVM 上下文中,接收器通常是 ViewModel 中需要调用的方法。

在这两者之间,有一个阻塞层,这意味着调用者和接收者不必明确地了解彼此。这通常表示为暴露给调用者的接口抽象,并且该接口的具体实现能够调用接收者。

让我们看一个简单的示例,您将在其中学习命令以及如何使用它们在 View 和 ViewModel 之间进行通信。在本章中,我们将继续使用上一章中的相同示例。

在 StudentView.xaml 文件中,我们有一个 ListBox,它连接 ViewModel 中的学生数据。现在让我们添加一个用于从列表框中删除学生的按钮。

重要的是,在按钮上使用命令非常容易,因为它们有一个命令属性可以连接到 ICommand。

因此,我们可以在 ViewModel 上公开一个具有 ICommand 的属性,并从按钮的命令属性绑定到它,如以下代码所示。

<Button Content = "Delete" 
   Command = "{Binding DeleteCommand}" 
   HorizontalAlignment = "Left" 
   VerticalAlignment = "Top" 
   Width = "75" />

让我们在项目中添加一个新类,它将实现 ICommand 接口。以下是ICommand接口的实现。

using System; 
using System.Windows.Input;

namespace MVVMDemo { 

   public class MyICommand : ICommand { 
      Action _TargetExecuteMethod; 
      Func<bool> _TargetCanExecuteMethod;
		
      public MyICommand(Action executeMethod) {
         _TargetExecuteMethod = executeMethod; 
      }
		
      public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){ 
         _TargetExecuteMethod = executeMethod;
         _TargetCanExecuteMethod = canExecuteMethod; 
      }
		
      public void RaiseCanExecuteChanged() { 
         CanExecuteChanged(this, EventArgs.Empty); 
      }
		
      bool ICommand.CanExecute(object parameter) { 
		
         if (_TargetCanExecuteMethod != null) { 
            return _TargetCanExecuteMethod(); 
         } 
			
         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(); 
         } 
      } 
   } 
}

正如您所看到的,这是 ICommand 的一个简单委托实现,其中我们有两个委托,一个用于executeMethod,另一个用于canExecuteMethod,可以在构造时传入。

在上面的实现中,有两个重载的构造函数,一个仅用于executeMethod,另一个用于两个executeMethod和我可以的canExecuteMethod。

让我们在 StudentView Model 类中添加一个 MyICommand 类型的属性。现在我们需要在 StudentViewModel 中构造一个实例。我们将使用带有两个参数的 MyICommand 的重载构造函数。

public MyICommand DeleteCommand { get; set;} 

public StudentViewModel() { 
   LoadStudents(); 
   DeleteCommand = new MyICommand(OnDelete, CanDelete); 
}

现在添加 OnDelete 和 CanDelete 方法的实现。

private void OnDelete() { 
   Students.Remove(SelectedStudent); 
}

private bool CanDelete() { 
   return SelectedStudent != null; 
}

我们还需要添加一个新的 SelectedStudent 以便用户可以从 ListBox 中删除所选项目。

private Student _selectedStudent;
 
public Student SelectedStudent { 
   get { 
      return _selectedStudent; 
   } 
	
   set { 
      _selectedStudent = value;
      DeleteCommand.RaiseCanExecuteChanged(); 
   } 
}

以下是 ViewModel 类的完整实现。

using MVVMDemo.Model; 

using System.Collections.ObjectModel; 
using System.Windows.Input; 
using System;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel { 
	
      public MyICommand DeleteCommand { get; set;} 
		
      public StudentViewModel() { 
         LoadStudents(); 
         DeleteCommand = new MyICommand(OnDelete, CanDelete); 
      }
		
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
			
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      }
		
      private Student _selectedStudent; 
		
      public Student SelectedStudent { 
         get {
            return _selectedStudent; 
         } 
			
         set { 
            _selectedStudent = value;
            DeleteCommand.RaiseCanExecuteChanged(); 
         } 
      }
		
      private void OnDelete() { 
         Students.Remove(SelectedStudent); 
      }
		
      private bool CanDelete() { 
         return SelectedStudent != null; 
      }
   } 
}

在 StudentView.xaml 中,我们需要在 ListBox 中添加 SelectedItem 属性,该属性将绑定到 SelectStudent 属性。

<ListBox ItemsSource = "{Binding Students}" SelectedItem = "{Binding SelectedStudent}"/>

以下是完整的 xaml 文件。

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:data = "clr-namespace:MVVMDemo.Model" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d"
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.Resources> 
      <DataTemplate DataType = "{x:Type data:Student}"> 
		
         <StackPanel Orientation = "Horizontal"> 
			
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
					
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>
	
   <Grid> 
      <StackPanel Orientation = "Horizontal"> 
         <ListBox ItemsSource = "{Binding Students}" 
            SelectedItem = "{Binding SelectedStudent}"/> 
				
         <Button Content = "Delete" 
            Command = "{Binding DeleteCommand}"
            HorizontalAlignment = "Left" 
            VerticalAlignment = "Top" 
            Width = "75" /> 
      </StackPanel> 
   </Grid>
	
</UserControl>

编译并执行上述代码后,您将看到以下窗口。

View 和 ViewModel 通信 MainWindow1

您可以看到删除按钮被禁用。当您选择任何项目时,它将启用。

View 和 ViewModel 通信 MainWindow2

当您选择任何项目并按删除时。您将看到所选项目列表被删除,并且删除按钮再次被禁用。

View 和 ViewModel 通信 MainWindow3

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

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 属性进行一些视图切换了。我们只需要某种方法来驱动该属性的设置。我们将做到这一点,以便最终用户可以命令进入客户列表