- MVVM 教程
- MVVM - 首页
- MVVM – 简介
- MVVM - 优点
- MVVM - 职责
- MVVM - 第一个应用
- MVVM - 关联视图
- MVVM - 关联ViewModel
- MVVM - WPF 数据绑定
- MVVM - WPF 数据模板
- MVVM - ViewModel 通信
- MVVM - 层次结构与导航
- MVVM - 验证
- MVVM - 依赖注入
- MVVM - 事件
- MVVM - 单元测试
- MVVM - 框架
- MVVM - 面试问题
- MVVM 有用资源
- MVVM 快速指南
- MVVM - 有用资源
- MVVM - 讨论
MVVM 快速指南
MVVM – 简介
组织代码井然有序且可能最可重用的方法是使用“MVVM”模式。模型、视图、ViewModel(MVVM 模式)旨在指导您如何组织和构建代码,以编写可维护、可测试和可扩展的应用程序。
模型 - 它只保存数据,与任何业务逻辑无关。
ViewModel - 它充当模型和视图之间的链接/连接,并使事物看起来更漂亮。
视图 - 它只保存格式化数据,并基本上将所有内容委托给模型。
分离的表示层
为了避免将应用程序逻辑放在代码隐藏或 XAML 中所导致的问题,最好使用一种称为分离表示层的技术。我们试图避免这种情况,在这种情况下,我们将拥有 XAML 和代码隐藏,其中包含与用户界面对象直接交互所需的最少内容。用户界面类还包含用于复杂交互行为、应用程序逻辑和其他所有内容的代码,如下面的左侧图所示。
使用分离的表示层,用户界面类会简单得多。它当然有 XAML,但代码隐藏尽可能少。
应用程序逻辑属于一个单独的类,通常称为模型。
然而,这并不是全部。如果您止步于此,您很可能会重复一个非常常见的错误,这将导致您走上数据绑定疯狂的道路。
许多开发人员尝试使用数据绑定将 XAML 中的元素直接连接到模型中的属性。
有时这可以,但通常不行。问题在于模型完全关注应用程序的功能,而不是用户如何与应用程序交互。
呈现数据的方式通常与数据在内部的结构方式有所不同。
此外,大多数用户界面都具有一些不属于应用程序模型的状态。
例如,如果您的用户界面使用拖放,则某些内容需要跟踪诸如当前拖动项的位置、其外观在移过可能的放置目标时应如何变化以及这些放置目标在拖动该项时如何变化等方面。
这种状态可能会变得非常复杂,需要进行彻底的测试。
在实践中,您通常希望在用户界面和模型之间放置另一个类。这有两个重要作用。
首先,它将您的应用程序模型适配到特定的用户界面视图。
其次,它是任何重要的交互逻辑所在之处,我的意思是让您的用户界面按您想要的方式运行所需的代码。
MVVM – 优点
MVVM 模式最终是 MVC 模式的现代结构,因此主要目标仍然相同,即在领域逻辑和表示层之间提供清晰的分离。以下是 MVVM 模式的优缺点。
主要好处是允许在视图和模型之间实现真正的分离,超越实现分离以及从拥有这种分离所获得的效率。在实际意义上,这意味着当您的模型需要更改时,它可以轻松更改,而无需更改视图,反之亦然。
应用 MVVM 会产生三个重要的关键要素,如下所示。
可维护性
不同类型代码的清晰分离应该使您更容易进入一个或多个更细粒度和更集中的部分进行更改,而无需担心。
这意味着您可以保持敏捷并快速发布新版本。
可测试性
使用 MVVM,每个代码部分都更细粒度,如果实现正确,您的外部和内部依赖项将与您想要测试的核心逻辑部分的代码分离。
这使得针对核心逻辑编写单元测试容易得多。
确保在编写时正确运行,即使在维护时发生更改也能保持运行。
可扩展性
由于清晰的分离边界和更细粒度的代码部分,它有时与可维护性重叠。
您更有可能使这些部分中的任何部分更可重用。
它还具有替换或添加执行类似操作的新代码部分到架构中正确位置的能力。
MVVM 模式的明显目的是抽象视图,从而减少代码隐藏中的业务逻辑量。但是,以下是一些其他的优点:
- ViewModel 比代码隐藏或事件驱动的代码更容易进行单元测试。
- 您可以无需笨拙的 UI 自动化和交互即可对其进行测试。
- 表示层和逻辑是松散耦合的。
缺点
- 有些人认为对于简单的 UI,MVVM 可能过于复杂。
- 同样,在更大的情况下,设计 ViewModel 可能很困难。
- 当我们有复杂的数据绑定时,调试会比较困难。
MVVM – 职责
MVVM 模式由三个部分组成:模型、视图和 ViewModel。大多数开发人员一开始对模型、视图和 ViewModel 应该包含什么或不应该包含什么以及每个部分的职责是什么感到有些困惑。
在本章中,我们将学习 MVVM 模式每个部分的职责,以便您可以清楚地理解哪种代码放在哪里。MVVM 实际上是客户端的分层架构,如下图所示。
表示层由视图组成。
逻辑层是视图模型。
表示层是模型对象的组合。
产生和持久化它们的客户端服务,在两层应用程序中直接访问,或者在然后到您的应用程序的服务调用中访问。
客户端服务并非 MVVM 模式的正式组成部分,但它经常与 MVVM 一起使用以实现进一步的分离并避免重复代码。
模型职责
通常,模型是最容易理解的。它是支持应用程序中视图的客户端数据模型。
它由具有属性和一些变量的对象组成,用于在内存中保存数据。
其中一些属性可能引用其他模型对象并创建对象图,该对象图作为一个整体是模型对象。
模型对象应该引发属性更改通知,在 WPF 中这意味着数据绑定。
最后一个职责是验证,这是可选的,但是您可以通过使用 WPF 数据绑定验证功能(通过诸如 INotifyDataErrorInfo/IDataErrorInfo 之类的接口)将验证信息嵌入到模型对象中。
视图职责
视图的主要目的和职责是定义用户在屏幕上看到的内容的结构。该结构可以包含静态部分和动态部分。
静态部分是定义视图由其组成的控件和控件布局的 XAML 层次结构。
动态部分就像定义为视图一部分的动画或状态更改。
MVVM 的主要目标是视图中不应该有代码隐藏。
视图中不可能没有代码隐藏。在视图中,您至少需要构造函数和对初始化组件的调用。
我们的想法是,事件处理、操作和数据操作逻辑代码不应放在视图中的代码隐藏中。
还有一些其他类型的代码必须放在代码隐藏中,任何需要引用 UI 元素的代码本质上都是视图代码。
ViewModel 职责
ViewModel 是 MVVM 应用程序的重点。ViewModel 的主要职责是向视图提供数据,以便视图可以将该数据放在屏幕上。
它还允许用户与数据交互并更改数据。
ViewModel 的另一个关键职责是封装视图的交互逻辑,但这并不意味着应用程序的所有逻辑都应该进入 ViewModel。
它应该能够处理适当的调用顺序,以便根据用户或视图上的任何更改做出正确的事情。
ViewModel 还应该管理任何导航逻辑,例如确定何时导航到不同的视图。
MVVM – 第一个应用程序
在本章中,我们将学习如何将 MVVM 模式用于简单的输入屏幕和您可能已经习惯使用的 WPF 应用程序。
让我们来看一个简单的示例,我们将在这个示例中使用 MVVM 方法。
步骤 1 - 创建一个新的 WPF 应用程序项目 MVVMDemo。
步骤 2 - 将三个文件夹(Model、ViewModel 和 Views)添加到您的项目中。
步骤 3 - 在 Model 文件夹中添加一个 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 - 在 ViewModel 文件夹中添加另一个 StudentViewModel 类,并粘贴以下代码。
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 - 通过右键单击 Views 文件夹并选择添加 > 新建项…来添加新的用户控件 (WPF)。
步骤 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 − 当以上代码编译并执行后,您将在主窗口上看到以下输出。
我们建议您逐步执行以上示例,以便更好地理解。
MVVM – 绑定视图
本章将介绍将视图绑定到 ViewModel 的不同方法。首先,让我们看一下视图优先构造,我们可以在 XAML 中声明它。正如我们在上一章中看到的示例,我们已经从主窗口绑定了一个视图。现在我们将看到其他绑定视图的方法。
本章也将使用相同的示例。以下是相同的 Model 类实现。
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 属性设置程序中的嵌套元素,如下面的代码所示。
<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.XAML 文件中显示的那样在 MainWindow 中声明此 View。
<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 的高级过程。
步骤 1 − 确定正在构造哪个视图类型。
步骤 2 − 识别该特定视图类型的 ViewModel。
步骤 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));
现在您可以看到一个基本的附加属性定义。要向属性添加行为,我们需要为此属性添加一个更改事件处理程序,其中包含自动绑定视图 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 正在为该特定视图绑定 ViewModel。
需要注意的关键一点是,视图不再以某种方式与其 ViewModel 的类型或其构造方式耦合。所有这些都已移动到 ViewModelLocator 内部的中心位置。
MVVM – WPF 数据绑定
本章将学习数据绑定如何支持 MVVM 模式。数据绑定是使 MVVM 与其他 UI 分离模式(如 MVC 和 MVP)不同的关键特性。
对于数据绑定,您需要构建一个视图或一组 UI 元素,然后您需要一些绑定将指向的其他对象。
视图中的 UI 元素绑定到 ViewModel 公开的属性。
视图和 ViewModel 的构造顺序取决于具体情况,正如我们已经介绍的视图优先构造一样。
视图和 ViewModel 被构造,并且视图的 DataContext 被设置为 ViewModel。
绑定可以是单向或双向数据绑定,以便在视图和 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 模型的属性也有其自己的各个绑定,这些绑定绑定到文本框和文本块。
ItemsControl 的 ItemSource 能够绑定到 Students 属性,因为视图的整体 DataContext 设置为 ViewModel。
这里的属性的各个绑定也是 DataContext 绑定,但由于 ItemSource 的工作方式,它们并没有绑定到 ViewModel 本身。
当项目源绑定到其集合时,它会在渲染时为每个项目渲染一个容器,并将该容器的 DataContext 设置为该项目。因此,每一行中每个文本框和文本块的整体 DataContext 将是集合中的单个 Student。您还可以看到,这些文本框的绑定是双向数据绑定,而文本块是单向数据绑定,因为您无法编辑文本块。
再次运行此应用程序时,您将看到以下输出。
现在让我们将第一行第二个文本框中的文本从 Allain 更改为 Upston,然后按 Tab 键失去焦点。您将看到文本块文本也已更新。
这是因为文本框的绑定设置为双向绑定,它也会更新模型,并且从模型中再次更新文本块。
MVVM – WPF 数据模板
模板描述了控件的整体外观和视觉外观。对于每个控件,都与其关联一个默认模板,该模板赋予该控件外观。在 WPF 应用程序中,当您想要自定义控件的视觉行为和视觉外观时,可以轻松创建您自己的模板。逻辑和模板之间的连接可以通过数据绑定来实现。
在 MVVM 中,还有另一种主要形式,称为 ViewModel 优先构造。
ViewModel 优先构造方法利用了 WPF 中隐式数据模板的功能。
隐式数据模板可以根据数据绑定渲染的数据对象的类型,从当前资源字典中自动选择适合的元素模板。首先,您需要有一些绑定到数据对象的元素。
让我们再次看一下我们的简单示例,在这个示例中,您将了解如何利用数据模板(特别是隐式数据模板)进行 ViewModel 优先构造。以下是我们的 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 文件,删除现有实现并在 Resources 部分定义数据模板。
<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}"/>
在 Resources 部分,DataTemplate 的键为 studentsTemplate,然后要实际使用该模板,我们需要使用 ListBox 的 ItemTemplate 属性。因此,现在您可以看到我们指示列表框使用该特定模板来渲染这些 Students。以下是 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 包含在文本块和文本框中显示的 Student 类对象数据。
为了使其成为隐式模板,我们需要从列表框中删除 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 数据,因为它会通过定位相应的数据模板来自动映射正在渲染的对象的类型。
我们建议您逐步执行上述示例,以便更好地理解。
MVVM – 视图/视图模型通信
在本章中,我们将学习如何向 MVVM 应用程序添加交互性以及如何干净地调用逻辑。您还将看到所有这些都是通过保持松散耦合和良好的结构来完成的,这是 MVVM 模式的核心。要理解这一切,首先让我们了解命令。
通过命令进行视图/视图模型通信
命令模式已被充分记录,并且几十年来一直频繁地使用设计模式。在此模式中,有两个主要参与者,调用者和接收者。
调用者
调用者是一段可以执行某些命令式逻辑的代码。
通常,它是在 UI 框架上下文中用户交互的 UI 元素。
它可能只是应用程序其他地方的另一块逻辑代码。
接收者
接收者是在调用者触发时要执行的逻辑。
在 MVVM 的上下文中,接收者通常是需要调用的视图模型中的方法。
在这两者之间,您有一个阻塞层,这意味着调用者和接收者不必显式地了解彼此。这通常表示为公开给调用者的接口抽象,并且该接口的具体实现能够调用接收者。
让我们来看一个简单的示例,您将在其中学习命令以及如何使用它们在视图和视图模型之间进行通信。在本章中,我们将继续使用上一章中的相同示例。
在 StudentView.xaml 文件中,我们有一个 ListBox,它连接来自视图模型的学生数据。现在让我们添加一个按钮,用于从 ListBox 中删除学生。
重要的是,在按钮上使用命令非常容易,因为它们具有一个命令属性来连接到 ICommand。
因此,我们可以在视图模型上公开一个具有 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(); } }
以下是视图模型类的完整实现。
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>
编译并执行上述代码后,您将看到以下窗口。
您可以看到删除按钮处于禁用状态。当您选择任何项目时,它将被启用。
当您选择任何项目并按删除键时。您将看到选定的项目列表已被删除,并且删除按钮再次被禁用。
我们建议您逐步执行以上示例,以便更好地理解。
MVVM – 层次结构和导航
在构建 MVVM 应用程序时,您通常会将复杂的信息屏幕分解成一组父视图和子视图,其中子视图包含在面板或容器控件中的父视图中,并形成自身使用的层次结构。
分解复杂的视图后,并不意味着您将其分解成自己的 XAML 文件的每个子内容块都需要成为 MVVM 视图。
内容块只是提供结构来将某些内容呈现到屏幕上,并且不支持用户对该内容进行任何输入或操作。
它可能不需要单独的视图模型,而只是一个基于父视图模型公开的属性进行渲染的 XAML 块。
最后,如果您有一个视图和视图模型的层次结构,则父视图模型可以成为通信中心,以便每个子视图模型尽可能地与其他子视图模型及其父级分离。
让我们来看一个示例,我们将在其中定义不同视图之间的简单层次结构。创建一个新的 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,我们将使用其内容属性并将其绑定到视图模型引用。
现在在资源字典中为每个视图定义数据模板。以下是 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 的实例时,它都会呈现一个 CustomerListView,并连接 ViewModel。它是一个 Order ViewModel,它将呈现 OrderView,依此类推。
现在我们需要一个具有 CurrentViewModel 属性以及一些逻辑和命令的视图模型,以便能够切换属性内 ViewModel 的当前引用。
让我们为此 MainWindow 创建一个名为 MainWindowViewModel 的视图模型。我们可以直接从 XAML 创建视图模型的实例,并使用它来设置窗口的 DataContext 属性。为此,我们需要创建一个基类来封装 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 内部。为此,我们将使用一个名为 onNavigate 的方法,该方法接受字符串目标并返回 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 类派生所有视图模型。编译并执行上述代码后,您将看到以下输出。
如您所见,我们在 MainWindow 上只添加了两个按钮和一个 CurrentViewModel。如果您单击任何按钮,它将导航到该特定视图。让我们单击“客户”按钮,您将看到显示 CustomerListView。
我们建议您逐步执行以上示例,以便更好地理解。
MVVM – 验证
在本章中,我们将学习有关验证的内容。我们还将研究一种使用 WPF 绑定已经支持的内容以干净的方式进行验证的方法,但将其与 MVVM 组件关联起来。
MVVM 中的验证
当您的应用程序开始接受来自最终用户的输入数据时,您需要考虑验证该输入。
确保它符合您的总体要求。
WPF 在绑定系统中具有一些出色的构建和功能,用于验证输入,并且在执行 MVVM 时您仍然可以利用所有这些功能。
请记住,支持您的验证并定义哪些规则存在于哪些属性应该成为模型或视图模型的一部分的逻辑,而不是视图本身。
您仍然可以使用 WPF 数据绑定支持的所有表达验证的方式,包括:
- 在设置属性时引发异常。
- 实现 IDataErrorInfo 接口。
- 实现 INotifyDataErrorInfo。
- 使用 WPF 验证规则。
通常,建议使用 INotifyDataErrorInfo,它是在 WPF .NET 4.5 中引入的,它支持查询与属性关联的错误的对象,并且它还修复了所有其他选项的一些不足之处。具体来说,它允许异步验证。它允许属性与其关联多个错误。
添加验证
让我们来看一个示例,我们将在其中向我们的输入视图添加验证支持,在大型应用程序中,您可能需要在应用程序中的许多地方使用它。有时在视图上,有时在视图模型上,有时在这些辅助对象上,它们是模型对象的包装器。
将验证支持放在您可以从中继承不同场景的公共基类中是一个好习惯。
基类将支持 INotifyDataErrorInfo,以便在属性更改时触发该验证。
创建并添加一个名为 ValidatableBindableBase 的新类。由于我们已经有一个用于属性更改处理的基类,让我们从中派生基类,并实现 INotifyDataErrorInfo 接口。
以下是 ValidatableBindableBase 类的实现。
using System; using System.Collections.Generic; using System.ComponentModel; //using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Windows.Controls; namespace MVVMHierarchiesDemo { public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo { private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>(); public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { }; public System.Collections.IEnumerable GetErrors(string propertyName) { if (_errors.ContainsKey(propertyName)) return _errors[propertyName]; else return null; } public bool HasErrors { get { return _errors.Count > 0; } } protected override void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null) { base.SetProperty<T>(ref member, val, propertyName); ValidateProperty(propertyName, val); } private void ValidateProperty<T>(string propertyName, T value) { var results = new List<ValidationResult>(); //ValidationContext context = new ValidationContext(this); //context.MemberName = propertyName; //Validator.TryValidateProperty(value, context, results); if (results.Any()) { //_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList(); } else { _errors.Remove(propertyName); } ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); } } }
现在在各自的文件夹中添加 AddEditCustomerView 和 AddEditCustomerViewModel。以下是 AddEditCustomerView.xaml 的代码。
<UserControl x:Class = "MVVMHierarchiesDemo.Views.AddEditCustomerView" 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> <Grid.RowDefinitions> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> </Grid.RowDefinitions> <Grid x:Name = "grid1" HorizontalAlignment = "Left" DataContext = "{Binding Customer}" Margin = "10,10,0,0" VerticalAlignment = "Top"> <Grid.ColumnDefinitions> <ColumnDefinition Width = "Auto" /> <ColumnDefinition Width = "Auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> </Grid.RowDefinitions> <Label Content = "First Name:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "0" VerticalAlignment = "Center" /> <TextBox x:Name = "firstNameTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "0" Text = "{Binding FirstName, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> <Label Content = "Last Name:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "1" VerticalAlignment = "Center" /> <TextBox x:Name = "lastNameTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "1" Text = "{Binding LastName, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> <Label Content = "Email:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "2" VerticalAlignment = "Center" /> <TextBox x:Name = "emailTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "2" Text = "{Binding Email, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> <Label Content = "Phone:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "3" VerticalAlignment = "Center" /> <TextBox x:Name = "phoneTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "3" Text = "{Binding Phone, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> </Grid> <Grid Grid.Row = "1"> <Button Content = "Save" Command = "{Binding SaveCommand}" HorizontalAlignment = "Left" Margin = "25,5,0,0" VerticalAlignment = "Top" Width = "75" /> <Button Content = "Add" Command = "{Binding SaveCommand}" HorizontalAlignment = "Left" Margin = "25,5,0,0" VerticalAlignment = "Top" Width = "75" /> <Button Content = "Cancel" Command = "{Binding CancelCommand}" HorizontalAlignment = "Left" Margin = "150,5,0,0" VerticalAlignment = "Top" Width = "75" /> </Grid> </Grid> </UserControl>
以下是 AddEditCustomerViewModel 的实现。
using MVVMHierarchiesDemo.Model; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo.ViewModel { class AddEditCustomerViewModel : BindableBase { public AddEditCustomerViewModel() { CancelCommand = new MyIcommand(OnCancel); SaveCommand = new MyIcommand(OnSave, CanSave); } private bool _EditMode; public bool EditMode { get { return _EditMode; } set { SetProperty(ref _EditMode, value);} } private SimpleEditableCustomer _Customer; public SimpleEditableCustomer Customer { get { return _Customer; } set { SetProperty(ref _Customer, value);} } private Customer _editingCustomer = null; public void SetCustomer(Customer cust) { _editingCustomer = cust; if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged; Customer = new SimpleEditableCustomer(); Customer.ErrorsChanged += RaiseCanExecuteChanged; CopyCustomer(cust, Customer); } private void RaiseCanExecuteChanged(object sender, EventArgs e) { SaveCommand.RaiseCanExecuteChanged(); } public MyIcommand CancelCommand { get; private set; } public MyIcommand SaveCommand { get; private set; } public event Action Done = delegate { }; private void OnCancel() { Done(); } private async void OnSave() { Done(); } private bool CanSave() { return !Customer.HasErrors; } } }
以下是 SimpleEditableCustomer 类的实现。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo.Model { public class SimpleEditableCustomer : ValidatableBindableBase { private Guid _id; public Guid Id { get { return _id; } set { SetProperty(ref _id, value); } } private string _firstName; [Required] public string FirstName { get { return _firstName; } set { SetProperty(ref _firstName, value); } } private string _lastName; [Required] public string LastName { get { return _lastName; } set { SetProperty(ref _lastName, value); } } private string _email; [EmailAddress] public string Email { get { return _email; } set { SetProperty(ref _email, value); } } private string _phone; [Phone] public string Phone { get { return _phone; } set { SetProperty(ref _phone, value); } } } }
编译并执行上述代码后,您将看到以下窗口。
当您按下“添加客户”按钮时,您将看到以下视图。当用户不填写任何字段时,它将被突出显示,并且保存按钮将被禁用。
MVVM – 依赖注入
在本章中,我们将简要讨论依赖注入。我们已经介绍了数据绑定使视图和视图模型彼此解耦,这允许它们在不知道通信另一端究竟发生了什么的情况下进行通信。
现在我们需要类似的东西来将我们的视图模型与客户端服务解耦。
在面向对象编程的早期,开发人员面临着在应用程序中创建和检索类实例的问题。已经为这个问题提出了各种解决方案。
过去几年,依赖注入和控制反转 (IoC) 在开发者中越来越受欢迎,并取代了一些较旧的解决方案,例如单例模式。
依赖注入/IoC容器
IoC 和依赖注入是两种密切相关的设计模式,容器基本上是一块基础设施代码,可以同时实现这两种模式。
IoC 模式是关于委托对象的构造责任,而依赖注入模式是关于向已构造的对象提供依赖项。
它们都可以被视为构造的两个阶段方法。当您使用容器时,容器承担以下几个责任:
- 根据请求构造对象。
- 容器将确定该对象依赖于什么。
- 构造这些依赖项。
- 将它们注入到正在构造的对象中。
- 递归地执行此过程。
让我们看看如何使用依赖注入来打破 ViewModel 和客户端服务之间的耦合。我们将使用与之相关的依赖注入来连接 AddEditCustomerViewModel 表单的保存处理。
首先,我们需要在项目的 Services 文件夹中创建一个新的接口。如果您的项目中没有 Services 文件夹,请先创建一个,然后在 Services 文件夹中添加以下接口。
using MVVMHierarchiesDemo.Model; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo.Services { public interface ICustomersRepository { Task<List<Customer>> GetCustomersAsync(); Task<Customer> GetCustomerAsync(Guid id); Task<Customer> AddCustomerAsync(Customer customer); Task<Customer> UpdateCustomerAsync(Customer customer); Task DeleteCustomerAsync(Guid customerId); } }
以下是 ICustomersRepository 的实现。
using MVVMHierarchiesDemo.Model; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo.Services { public class CustomersRepository : ICustomersRepository { ZzaDbContext _context = new ZzaDbContext(); public Task<List<Customer>> GetCustomersAsync() { return _context.Customers.ToListAsync(); } public Task<Customer> GetCustomerAsync(Guid id) { return _context.Customers.FirstOrDefaultAsync(c => c.Id == id); } public async Task<Customer> AddCustomerAsync(Customer customer){ _context.Customers.Add(customer); await _context.SaveChangesAsync(); return customer; } public async Task<Customer> UpdateCustomerAsync(Customer customer) { if (!_context.Customers.Local.Any(c => c.Id == customer.Id)) { _context.Customers.Attach(customer); } _context.Entry(customer).State = EntityState.Modified; await _context.SaveChangesAsync(); return customer; } public async Task DeleteCustomerAsync(Guid customerId) { var customer = _context.Customers.FirstOrDefault(c => c.Id == customerId); if (customer != null) { _context.Customers.Remove(customer); } await _context.SaveChangesAsync(); } } }
简单的保存处理方法是在 AddEditCustomerViewModel 中添加 ICustomersRepository 的一个新实例,并重载 AddEditCustomerViewModel 和 CustomerListViewModel 构造函数。
private ICustomersRepository _repo; public AddEditCustomerViewModel(ICustomersRepository repo) { _repo = repo; CancelCommand = new MyIcommand(OnCancel); SaveCommand = new MyIcommand(OnSave, CanSave); }
按如下代码所示更新 OnSave 方法。
private async void OnSave() { UpdateCustomer(Customer, _editingCustomer); if (EditMode) await _repo.UpdateCustomerAsync(_editingCustomer); else await _repo.AddCustomerAsync(_editingCustomer); Done(); } private void UpdateCustomer(SimpleEditableCustomer source, Customer target) { target.FirstName = source.FirstName; target.LastName = source.LastName; target.Phone = source.Phone; target.Email = source.Email; }
以下是完整的 AddEditCustomerViewModel。
using MVVMHierarchiesDemo.Model; using MVVMHierarchiesDemo.Services; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo.ViewModel { class AddEditCustomerViewModel : BindableBase { private ICustomersRepository _repo; public AddEditCustomerViewModel(ICustomersRepository repo) { _repo = repo; CancelCommand = new MyIcommand(OnCancel); SaveCommand = new MyIcommand(OnSave, CanSave); } private bool _EditMode; public bool EditMode { get { return _EditMode; } set { SetProperty(ref _EditMode, value); } } private SimpleEditableCustomer _Customer; public SimpleEditableCustomer Customer { get { return _Customer; } set { SetProperty(ref _Customer, value); } } private Customer _editingCustomer = null; public void SetCustomer(Customer cust) { _editingCustomer = cust; if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged; Customer = new SimpleEditableCustomer(); Customer.ErrorsChanged += RaiseCanExecuteChanged; CopyCustomer(cust, Customer); } private void RaiseCanExecuteChanged(object sender, EventArgs e) { SaveCommand.RaiseCanExecuteChanged(); } public MyIcommand CancelCommand { get; private set; } public MyIcommand SaveCommand { get; private set; } public event Action Done = delegate { }; private void OnCancel() { Done(); } private async void OnSave() { UpdateCustomer(Customer, _editingCustomer); if (EditMode) await _repo.UpdateCustomerAsync(_editingCustomer); else await _repo.AddCustomerAsync(_editingCustomer); Done(); } private void UpdateCustomer(SimpleEditableCustomer source, Customer target) { target.FirstName = source.FirstName; target.LastName = source.LastName; target.Phone = source.Phone; target.Email = source.Email; } private bool CanSave() { return !Customer.HasErrors; } private void CopyCustomer(Customer source, SimpleEditableCustomer target) { target.Id = source.Id; if (EditMode) { target.FirstName = source.FirstName; target.LastName = source.LastName; target.Phone = source.Phone; target.Email = source.Email; } } } }
编译并执行上述代码后,您将看到相同的输出,但是现在 ViewModel 的耦合度更低了。
当您按下“添加客户”按钮时,您将看到以下视图。如果用户留下任何字段为空,则该字段将突出显示,并且保存按钮将被禁用。
MVVM – 事件
事件是一种编程结构,它对状态变化做出反应,通知已注册接收通知的任何端点。主要来说,事件用于通过鼠标和键盘告知用户输入,但其用途并不限于此。每当检测到状态更改时,例如当对象已加载或初始化时,都可以触发事件以提醒任何感兴趣的第三方。
在使用 MVVM(模型-视图-视图模型)设计模式的 WPF 应用程序中,视图模型是负责处理应用程序的表示逻辑和状态的组件。
视图的代码隐藏文件不应包含任何处理从任何用户界面 (UI) 元素(例如按钮或组合框)引发的事件的代码,也不应包含任何特定于域的逻辑。
理想情况下,视图的代码隐藏只包含调用 InitializeComponent 方法的构造函数,以及可能还有一些其他代码来控制或交互难以或效率低下地在 XAML 中表达的视图层,例如复杂的动画。
让我们来看一个应用程序中按钮单击事件的简单示例。以下是 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>
您可以看到,上述 XAML 文件中未使用按钮 Click 属性,而是使用了 Command 和 CommandParameter 属性来在按下按钮时加载不同的视图。现在您需要在 MainWindowViewModel.cs 文件中定义命令的实现,而不是在视图文件中。以下是完整的 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 类派生所有视图模型。编译并执行上述代码后,您将看到以下输出。
如您所见,我们只在 MainWindow 上添加了两个按钮和一个 CurrentViewModel。现在,如果您单击任何按钮,它将导航到该特定视图。让我们单击“客户”按钮,您将看到显示 CustomerListView。
我们建议您逐步执行上述示例,以便更好地理解。
MVVM – 单元测试
单元测试背后的理念是获取离散的代码块(单元),并编写测试方法以预期的方式使用代码,然后测试它们是否获得预期结果。
作为代码本身,单元测试与项目的其余部分一样进行编译。
它们也由测试运行软件执行,该软件可以快速运行每个测试,有效地给出赞成或反对的意见,以指示测试是否分别通过或失败。
让我们来看一个前面创建的示例。以下是学生模型的实现。
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)); } } } }
以下是 StudentView 的实现。
<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>
以下是 StudentViewModel 的实现。
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; } public int GetStudentCount() { return Students.Count; } } }
以下是 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>
以下是 MyICommand 的实现,它实现了 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(); } } } }
当以上代码编译并执行后,您将在主窗口上看到以下输出。
要为上述示例编写单元测试,让我们向解决方案添加一个新的测试项目。
通过右键单击“引用”来添加对项目的引用。
选择现有项目并单击“确定”。
现在让我们添加一个简单的测试,它将检查学生数量,如下面的代码所示。
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using MVVMDemo.ViewModel; namespace MVVMTest { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { StudentViewModel sViewModel = new StudentViewModel(); int count = sViewModel.GetStudentCount(); Assert.IsTrue(count == 3); } } }
要执行此测试,请选择“测试”→“运行”→“所有测试”菜单选项。
您可以在测试资源管理器中看到测试已通过,因为在 StudentViewModel 中添加了三个学生。将计数条件从 3 更改为 4,如下面的代码所示。
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using MVVMDemo.ViewModel; namespace MVVMTest { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { StudentViewModel sViewModel = new StudentViewModel(); int count = sViewModel.GetStudentCount(); Assert.IsTrue(count == 4); } } }
再次执行测试计划时,您将看到测试失败,因为学生数量不等于 4。
我们建议您逐步执行上述示例,以便更好地理解。
MVVM – 框架
在本章中,我们将讨论可用的 MVVM 工具包或框架。您也可以使用这些框架,这样您就不必编写大量重复的代码来自己实现 MVVM 模式。以下是一些最流行的框架:
- Prism
- MVVM Light
- Caliburn Micro
Prism
Prism 提供示例和文档形式的指导,帮助您轻松设计和构建丰富、灵活且易于维护的 Windows Presentation Foundation (WPF) 桌面应用程序。使用 Microsoft Silverlight 浏览器插件和 Windows 应用程序构建的富互联网应用程序 (RIA)。
Prism 使用体现重要架构设计原则(例如关注点分离和松散耦合)的设计模式。
Prism 帮助您使用松散耦合的组件设计和构建应用程序,这些组件可以独立发展,但可以轻松且无缝地集成到整个应用程序中。
这些类型的应用程序被称为组合应用程序。
Prism 具有许多开箱即用的功能。以下是 Prism 的一些重要功能。
MVVM 模式
Prism 支持 MVVM 模式。它有一个 Bindablebase 类,类似于前面章节中实现的类。
它有一个灵活的 ViewModelLocator,它具有约定,但允许您覆盖这些约定并以松散耦合的方式声明性地连接您的视图和 ViewModel。
模块化
这是能够将代码分解成完全松散耦合的类库部件,并在运行时将它们组合成一个对最终用户而言具有凝聚力的整体,而代码仍然完全解耦。
UI 组合/区域
这是能够将视图插入容器的能力,而无需进行插入的视图拥有对 UI 容器本身的显式引用。
导航
Prism 具有构建在区域之上的导航功能,例如向前和向后导航以及允许您的视图模型直接参与导航过程的导航堆栈。
命令
Prism 具有命令,因此它们具有一个委托命令,与我们在前面章节中使用的 MyICommand 非常相似,只是它具有一些额外的鲁棒性来保护您免受内存泄漏。
发布/订阅事件
Prism 还支持发布/订阅事件。这些是松散耦合的事件,其中发布者和订阅者可以具有不同的生命周期,并且不必具有彼此的显式引用即可通过事件进行通信。
MVVM Light
MVVM Light 由 Laurent Bugnion 制作,可以帮助您将视图与模型分离,从而创建更清晰、更易于维护和扩展的应用程序。
它还可以创建可测试的应用程序,并允许您拥有更精简的用户界面层(更难以自动测试)。
此工具包特别强调打开和编辑用户界面到 Blend 中,包括创建设计时数据,使 Blend 用户在使用数据控件时能够“看到一些东西”。
Caliburn Micro
这是另一个小型开源框架,可帮助您实现 MVVM 模式,并开箱即用地支持许多功能。
Caliburn Micro 是一个小型但功能强大的框架,专为跨所有 XAML 平台构建应用程序而设计。
凭借对 MVVM 和其他成熟的 UI 模式的强大支持,Caliburn Micro 将使您能够快速构建解决方案,而无需牺牲代码质量或可测试性。