当 Windows Presentation Foundation (WPF) 于 2006 年作为 .NET Framework 版本 3.0 的一部分首次发布时,它被宣传为桌面应用程序图形用户界面 (GUI) 语言的未来,支持者声称它将结束以前的 GUI 技术、Windows 窗体。 然而,随着时间的推移,它与这一说法相去甚远。
WPF 没有像之前预期的那样广泛普及主要有以下三个原因。 第一个原因与 WPF 无关,源于最近推动将所有内容托管在云中并拥有 Web 界面而不是桌面应用程序。 第二个原因与掌握 WPF 所需的非常陡峭的学习曲线和非常不同的工作方式有关。
最后一个原因是它不是一种非常高效的语言,如果 WPF 应用程序有很多“花哨的东西”,那么客户端计算机将需要安装额外的 RAM 和/或显卡,否则它们可能会面临 缓慢且口吃的用户体验。
这解释了为什么当今许多使用 WPF 的公司都属于金融行业,他们有能力升级所有用户的计算机,以便能够以最佳方式运行其应用程序。 本书旨在通过提供实用的提示和技巧来帮助我们更轻松、更高效地构建现实世界的应用程序,从而使我们其他人更容易使用 WPF。
为了改进 WPF 的工作方式,我们可以进行的最简单的更改和最大的工作流程改进之一是遵循 MVVM 软件架构模式。 它描述了我们如何组织我们的类,使我们的应用程序更易于维护、可测试,并且通常更易于理解。 在本章中,我们将仔细研究这种模式,并发现它如何帮助我们改进应用程序。
在了解什么是 MVVM 及其优点之后,我们将学习在这个新环境中的各个组件之间进行通信的几种新方法。 然后,我们将重点关注典型 MVVM 应用程序中代码库的物理结构,并研究各种替代安排。
什么是 MVVM,它有什么帮助?
模型-视图-视图模型 (MVVM) 是一种软件架构模式,由 John Gossman 于 2005 年在他的博客上提出,现在在开发 WPF 应用程序时普遍使用。 其主要目的是在业务模型、用户界面 (UI) 和业务逻辑之间提供关注点分离。 它通过将它们分为三种不同类型的核心组件来实现这一点:模型、视图和视图模型。 让我们看一下它们是如何排列的以及每个组件代表什么:
正如我们在这里看到的,视图模型组件位于模型和视图之间,并提供对它们中的每一个的双向访问。 此时应该注意的是,视图和模型组件之间不应该有直接关系,而其他组件之间只有松散的连接。 现在让我们仔细看看每个组件代表什么。
模型
与其他 MVVM 组件不同,模型组件由许多元素组成。 它包含业务数据模型及其相关的验证逻辑,以及为应用程序提供数据访问和持久性的数据访问层 (DAL) 或数据存储库。
数据模型表示在应用程序中保存数据的类。 它们通常或多或少地镜像数据库中的列,尽管它们通常在形式上是分层的,因此可能需要在数据源中执行联接才能完全填充它们。
一种替代方法是设计数据模型类来满足 UI 中的要求,但无论哪种方式,业务逻辑或验证规则通常都将与数据模型驻留在同一项目中。
用于与我们的应用程序中使用的任何数据持久性技术进行交互的代码也包含在该模式的模型组件中。 在代码库中组织此组件时应小心,因为有许多问题需要考虑。 我们稍后将对此进行进一步研究,但现在,让我们继续了解有关此模式中的组件的更多信息。
视图模型
视图模型可以很容易地解释; 每个视图模型为其关联的视图提供其所需的所有数据和功能。 在某些方面,它们可以被认为与文件背后的旧 Windows 窗体代码类似,只是它们与它们所服务的视图没有直接关系。 如果您熟悉 MVC,更好的类比是它们类似于模型-视图-控制器 (MVC) 软件架构模式中的控制器。 事实上,John 在他的博客中将 MVVM 模式描述为 MVC 模式的变体。
它们与模型组件有双向连接,以便访问和更新视图所需的数据,并且通常,它们以某种方式转换该数据,以便更容易在 UI 中显示和交互。 它们还通过数据绑定和属性更改通知与视图建立双向连接。 简而言之,视图模型形成了模型和视图之间的桥梁,否则模型和视图之间没有任何联系。
但是,应该注意的是,视图模型仅通过数据绑定和接口与视图和模型组件松散地连接。 这种图案的美丽使每个元素都能够彼此独立地发挥作用。
为了保持视图模型和视图之间的分离,我们避免在视图模型中声明与 UI 相关的类型的任何属性。 我们不希望在视图模型项目中引用任何与 UI 相关的 DLL,因此我们使用自定义 IValueConverter
实现将它们转换为原始类型。 例如,我们可以将 UI 中的 Visibility
对象转换为纯布尔值,或者将某些彩色 Brush
对象的选择转换为可在视图模型中安全使用的 Enum
实例。 我们将在本书中看到几个转换器的示例,但现在让我们继续。
视图
视图定义 UI 的外观和布局。 它们通常通过使用 DataContext
属性与视图模型连接并显示它提供的数据。 它们通过将视图模型的命令连接到用户与之交互的 UI 控件来公开视图模型提供的功能。
一般来说,基本的经验法则是每个视图都有一个关联的视图模型。 这并不意味着视图不能将数据绑定到多个数据源,也不意味着我们不能重用视图模型。 它只是意味着,一般来说,如果我们有一个名为 SecurityView 的类,那么我们很可能还会有一个名为 SecurityViewModel
的类的实例,该实例将被设置为该视图的 DataContext 属性的值。
数据绑定
MVVM 模式经常被忽视的一个方面是它对数据绑定的要求。 没有它,我们就无法实现完全的关注点分离,因为视图和视图模型之间没有简单的通信方式。 XAML 标记、数据绑定类以及 ICommand
和 INotifyPropertyChanged
接口是 WPF 中提供此功能的主要工具。
ICommand
接口是在 .NET Framework 中实现命令的方式。 它提供的行为实现甚至扩展了非常有用的命令模式,其中对象封装了执行操作所需的所有内容。 WPF 中的大多数 UI 控件都具有 Command 属性,我们可以使用这些属性将它们连接到命令提供的功能。
INotifyPropertyChanged
接口用于通知绑定客户端属性值已更改。 例如,如果我们有一个 User 对象并且它有一个 Name 属性,那么我们的 User 类将负责引发 INotifyPropertyChanged
接口的 PropertyChanged
事件,并在每次更改属性值时指定属性的名称。 稍后我们将更深入地研究所有这些,但现在让我们看看这些组件的排列如何帮助我们。
那么 MVVM 有什么帮助呢?
采用 MVVM 的一大好处是它提供了业务模型、UI 和业务逻辑之间至关重要的关注点分离。 这使我们能够做几件事。 它将视图模型从模型(业务模型和数据持久技术)中解放出来。
这反过来又使我们能够在其他应用程序中重用业务模型,并替换 DAL 并将其替换为模拟数据层,以便我们可以有效地测试视图模型中的功能,而不需要任何类型的实际数据连接。
它还断开视图与它们所需的视图逻辑的连接,因为这是由视图模型提供的。 这使我们能够独立运行每个组件,其优点是使一个团队可以设计视图,而另一个团队可以处理视图模型。 并行工作流使公司能够从大大缩短的生产时间中受益。
此外,这种分离还使我们更容易将视图替换为不同的技术,而无需更改模型代码。 我们很可能需要更改视图模型的某些方面,例如,用于视图的新技术可能不支持 ICommand
接口,但原则上,我们需要更改的代码量将相当少。
MVVM 模式的简单性也使 WPF 更易于理解。 知道每个视图都有一个视图模型,为它提供所需的所有数据和功能,这意味着当我们想要查找数据绑定属性的声明位置时,我们始终知道在哪里查找。
有缺点吗?
然而,使用 MVVM 也有一些缺点,而且它并不能在所有情况下为我们提供帮助。 实现 MVVM 的主要缺点是它给我们的应用程序增加了一定程度的复杂性。 首先是数据绑定,这可能需要一些时间来掌握。 此外,根据您的 Visual Studio 版本,数据绑定错误可能仅在运行时出现,并且很难追踪。 不过,我们将在下一章中研究解决方案。
然后,我们需要了解视图和视图模型之间进行通信的不同方式。 以不寻常的方式命令和处理事件需要一段时间才能习惯。 必须发现代码库中所有必需组件的最佳排列也需要时间。 因此,在我们确实能够胜任实现 MVVM 之前,还有一个陡峭的学习曲线需要攀爬。 本书将详细涵盖所有这些领域,并试图减少学习曲线的梯度。
然而,即使我们很好地练习了该模式,仍然偶尔会出现实现 MVVM 没有意义的情况。 一个例子是,如果我们的应用程序非常小,我们就不太可能想要对其进行单元测试或更换其任何组件。 因此,当不需要模式提供的关注点分离的好处时,增加实现该模式的复杂性是不切实际的。
揭穿有关代码隐藏的神话
关于 MVVM 的最大误解之一是我们应该避免将任何代码放入视图文件的代码隐藏中。 虽然这有一定道理,但并非在所有情况下都是如此。 如果我们逻辑思考一下,我们已经知道使用 MVVM 的主要原因是利用其架构提供的关注点分离。 其中一部分将视图模型中的业务功能与视图中与用户界面相关的代码分开。 因此,规则实际上应该是我们应该避免将任何业务逻辑放入视图文件背后的代码中。
记住这一点,让我们看看我们可能想要将哪些代码放入视图的代码隐藏文件中。 最有可能的嫌疑是一些与 UI 相关的代码,可能处理特定事件,或者启动某种子窗口。 在这些情况下,使用文件背后的代码绝对没问题。 我们这里没有业务相关的代码,因此我们不需要将其与其他 UI 相关的代码分开。
另一方面,如果我们在View
的代码隐藏文件中编写了一些与业务相关的代码,那么我们如何测试它呢? 在这种情况下,我们将无法将其与视图分离,不再有我们的关注点分离,因此,将破坏我们的 MVVM 实现。 因此,在这种情况下,神话不再是神话……这是个好建议。
然而,即使在这样的情况下,我们想要从视图中调用一些与业务相关的代码,也可以在不违反任何规则的情况下实现。 只要我们的业务代码驻留在视图模型中,就可以通过该视图模型进行测试,因此在运行时从哪里调用它并不那么重要。 了解我们始终可以访问将数据绑定到视图的 DataContext
属性的视图模型,让我们看一下这个简单的示例:
private void Button_Click(object sender, RoutedEventArgs e)
{
UserViewModel viewModel = (UserViewModel)DataContext;
viewModel.PerformSomeAction();
}
现在,有些人会对这个代码示例犹豫不决,因为他们正确地认为视图不应该知道有关其相关视图模型的任何信息。 此代码有效地将视图模型与视图联系起来。 如果我们想在某个时候更改应用程序中的 UI 层或让设计人员在视图上工作,那么这段代码会给我们带来问题。 然而,我们需要现实一些……我们真正需要这样做的可能性有多大?
如果可能的话,那么我们真的不应该将此代码放入代码隐藏文件中,而是通过将其包装在附加属性中来处理事件,我们将在下一节中看到这样的示例。 不过,如果根本不可能的话,那么放在那里确实没有问题。
例如,如果我们有一个 UserView
,它有一个附带的 UserViewModel
类,并且我们确定不需要更改它,那么在这种情况下,我们可以安全地使用上面的代码,而不必担心直接转换会导致异常 被扔掉。 当规则对我们有意义时,让我们遵守它们,而不是盲目地遵守它们,因为在不同情况下有人说这是个好主意。
另一种我们可以忽略“无代码隐藏”规则的情况是基于 UserControl
类编写独立控件时。 在这些情况下,文件背后的代码通常用于定义依赖属性和/或处理 UI 事件以及实现一般 UI 功能。 但请记住,如果这些控件正在实现一些与业务相关的功能,我们应该将其写入视图模型并从控件中调用它,以便仍然可以对其进行测试。
避免在视图的代码隐藏文件中编写与业务相关的代码的一般想法绝对是完美的,我们应该始终尝试这样做。 然而,我们现在希望了解这个想法背后的原因,并可以使用我们的逻辑来确定在可能出现的每种特定情况下是否可以这样做。
学习如何再次沟通
由于我们往往不直接处理 UI 事件,因此在使用 MVVM 时,我们需要其他方法来实现它们提供的相同功能。 需要不同的方法来重现不同事件的功能。 例如,集合控件的 SelectionChanged
事件的功能通常通过将视图模型属性数据绑定到集合控件的 SelectedItem
属性来重现:
<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding CurrentItem}" />
在此示例中,每次从 ListBox
中选择新项目时,WPF 框架都会调用 CurrentItem
属性的 setter
。 因此,我们可以直接从视图模型中的属性设置器调用任何方法,而不是在后面的代码中处理 SelectionChanged
事件:
public TypeOfObject CurrentItem
{
get {
return currentItem; }
set
{
currentItem = value;
DoSomethingWithTheNewlySelectedItem(currentItem);
}
}
请注意,我们需要防止从数据绑定属性设置器调用的任何方法执行过多操作,因为执行它们所需的时间可能会对输入数据时的性能产生负面影响。 但是,在此示例中,我们通常会使用此方法来使用当前项中的值启动异步数据访问函数,或更改视图模型中另一个属性的值。
许多其他 UI 事件可以直接替换为 XAML 标记中某种形式的触发器。 例如,假设我们有一个被设置为 Button
控件的 Content
属性值的 Image
元素,并且我们希望在禁用 Button
时 Image
元素是半透明的。 我们可以在可应用于 Image
元素的 Style
中编写 DataTrigger
,而不是在代码隐藏文件中处理 UIElement.IsEnabledChanged
事件:
<Style x:Key="ImageInButtonStyle" TargetType="{x:Type Image}">
<Setter Property="Opacity" Value="1.0" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Button}}, FallbackValue=False}"
Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
绑定语法将在第 4 章“精通数据绑定”中详细介绍,但简而言之,此 DataTrigger
中的绑定将目标指定为 Button
类型的 Image
元素的祖先(或父级)的 IsEnabled
属性 。 当此绑定目标的值为 False
时,图像的 Opacity
属性将设置为 0.5
,并在目标属性值为 True
时设置回其原始值。 因此,当按钮被禁用时,按钮中的图像元素将变得半透明。
介绍 ICommand 接口
当涉及 WPF 和 MVVM 中的按钮单击时,我们通常使用某种形式的实现 ICommand
接口的命令,而不是处理众所周知的 Click 事件。 让我们看一个基本标准命令的示例:
using System;
using System.Windows.Forms;
using System.Windows.Input;
public class TestCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
MessageBox.