什么是 MVVM?
不使用模型-视图-视图模型 (MVVM) 的 .NET MAUI 应用的代码隐藏文件中通常包含更多的代码。 .NET MAUI 中的代码隐藏文件遵循以下模式:{something}.xaml.cs。 代码隐藏文件中的大部分代码通常控制用户界面 (UI) 行为。 UI 行为可包括 UI 发生的任何事情,例如更改颜色或更改部分文本。 它还可包括因 UI 发生的任何事情,包括按钮单击处理程序。
使用此方法存在的问题是难以针对代码隐藏文件编写单元测试。 代码隐藏文件通常假定应用程序状态是通过分析 XAML 创建的,甚至是由其他页面创建的。 在(甚至可能不在移动设备上运行的)单元测试运行器中很难处理这些条件,更不用说用户界面了。 因此,单元测试很少能测试这些文件中的 UI 行为。
但在这里,MVVM 模式非常有用。 在正确使用的前提下,MVVM 模式会通过将大多数 UI 行为逻辑移动到名为 ViewModel 的、可进行单元测试的类中,来解决这些问题。 MVVM 模式最常用于支持数据绑定的框架。 使用 .NET MAUI,你可以将每个 UI 元素数据绑定到一个 viewmodel
,且无需或几乎不需要使用视图或代码隐藏文件中的代码。
MVVM 应用程序的组成部分有哪些?
虽然viewmodel
是 MVVM 模式的独特部分,但是该模式还定义了“模型”部分和“视图”部分。 这些部分的定义与其他一些常见模式(如“模型-视图-控制器”(MVC))是一致的。
什么是模型?
在 MVVM 应用程序中,“模型”一词用于表示业务数据和操作。 模型本身不涉及应用的用户演示文稿。
用于确定什么代码属于模型的有用规则是,它应该可跨不同的平台移植。 从移动应用到 Web 界面甚至命令行程序,在所有实例中使用相同模型。 它与向用户显示信息的方式无关。
对于方案中的 HR 应用程序,模型可能包含 Employee
类和 Department
类,这些类包含关于这些实体的数据和逻辑。 模型还可能包含类似 EmployeeRepository
类的内容,该类包含暂留逻辑。 其他一些软件设计模式将存储库等内容视为独立于模型的内容。 但是在 MVVM 的上下文中,我们通常将所有业务逻辑或业务数据称为模型的一部分。
下面是 C# 中模型的两个示例:
public class Employee
{
public int Id { get; }
public string Name { get; set; }
public Employee Supervisor { get; set; }
public DateTime HireDate { get; set; }
public void ClockIn() { ... }
}
public class EmployeeRepository
{
public IList<Employee> QueryEmployees() { ... }
...
}
什么是视图?
视图代码控制直接与用户交互的操作,如按钮和输入字段等控件,以及其他纯视觉元素,如主题、样式和字体。
在 .NET MAUI 中,无需编写任何 C# 代码来自行生成视图。 相反,通常按 XAML 文件定义视图。 在某些情况下,需要自定义用户控件,在这种情况下,你可以通过代码来创建自己的视图。
什么是viewmodel
?
现在我们回过来讨论viewmodel
。 viewmodel
充当业务逻辑(模型)和视图 (UI) 之间的中介。
考虑viewmodel
对 HR 应用程序可能执行的操作。 假设有一个视图显示员工的可休假时间,并且你希望假期余额显示为“2 周 3 天 4 小时”。但模型中的业务逻辑提供了相同的值,即 13.5 天,这是一个十进制数,表示按 8 小时工作日计的总天数。 对象模型可能如以下列表所示:
模型 -
Employee
类,其中包含一种方法:public decimal GetVacationBalanceInDays() { //Some math that calculates current vacation balance ... }
视图模型 - 一个
EmployeeViewModel
类,具有如下属性:public class EmployeeViewModel { private Employee _model; public string FormattedVacationBalance { get { decimal vacationBalance = _model.GetVacationBalanceInDays(); ... // Some logic to format and return the string as "X weeks, Y days, Z hours" } } }
视图 - 一个包含单个标签和关闭按钮的 XAML 页面。 该标签带有对
viewmodel
属性的绑定:<Label Text="{Binding FormattedVacationBalance}" />
只需将页面的
BindingContext
设置为EmployeeViewModel
的实例。
在此示例中,模型包含业务逻辑。 此逻辑未绑定到视觉显示或设备。 可以将相同的逻辑用于手持设备或台式计算机。 视图不了解业务逻辑。 视图控件(如标签)知道如何在屏幕上获取文本,但并不关心它是假期余额还是随机字符串。 viewmodel
对这两方面都略有了解,所以可以作为中介。
我们来看看 viewmodel
是如何成为中介的:它公开视图可以绑定的属性。 公共属性是 viewmodel
提供数据的唯一途径。 viewmodel
得名的原因是 MVVM 中的“Model”(模型)表示业务流程的结构、数据和逻辑,而“viewmodel
”表示视图所需的结构、数据和逻辑。
视图如何与viewmodel
配合使用?
当你查看viewmodel
与模型的关系时,它是标准的类到类关系。 viewmodel
具有模型的实例,并通过属性向视图公开模型的各个方面。 但是,视图如何获取和设置viewmodel
的属性呢? 当viewmodel
发生更改时,视图如何使用新值更新视觉对象控件? 答案是数据绑定。
在对象绑定到视图时读取viewmodel
的属性。 但是,绑定操作无法知道在绑定应用于视图后,viewmodel
的属性是否发生更改。 更改viewmodel
不会通过绑定到视图自动传播新值。 若要从viewmodel
更新到视图,viewmodel
必须实现 System.ComponentModel.INotifyPropertyChanged
接口。
INotifyPropertyChanged
接口声明名为 PropertyChanged
的单个事件。 它采用单个参数,即更改其值的属性的名称。 .NET MAUI 中使用的绑定系统了解此接口并侦听该事件。 当viewmodel
上的属性发生更改并引发事件时,绑定会将更改通知目标。
请考虑此关系在使用员工 viewmodel
的 HR 应用程序中的运作方式。 viewmodel
具有表示员工的属性,例如员工的姓名。 viewmodel
实现 INotifyPropertyChanged
接口,当 Name
属性发生更改时,会引发 PropertyChanged
事件,如下所示:
using System.ComponentModel;
public class EmployeeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private Employee _model;
public string Name
{
get {...}
set
{
_model.Name = value;
OnPropertyChanged(nameof(Name))
}
}
protected void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
描述员工详细信息的视图包含绑定到 viewmodel
的 Name
属性的标签控件:
<Label Text="{Binding Name}" />
当viewmodel
中的 Name
属性发生更改时,会用该属性的名称引发 PropertyChanged
事件。 绑定侦听事件,然后通知标签 Name
属性已更改。 随后,标签的 Text
属性将更新为最新值。