使用 ViewModel

已完成

了解构成模型-视图-视图模型 (MVVM) 模式的组件后,你可能发现模型和视图易于定义。 接下来,我们了解如何使用视图模型,以便更好地定义其在模式中的角色。

向用户界面公开属性

与上述示例一样,ViewModel 的大部分数据和所有业务逻辑通常都依赖于模型。 但以当前视图所需的任何方式设置数据的格式、转化数据或丰富数据的却是 ViewModel。

使用 ViewModel 设置格式

你已看过假期时间格式设置的示例。 日期格式设置、字符编码和序列化都是关于 ViewModel 如何设置模型中数据的格式的示例。

使用 ViewModel 进行转换

通常情况下,模型以间接方式提供信息。 不过 ViewModel 可以解决此问题。 例如,假设需要在屏幕上显示一名员工是否是主管。 但 Employee 模型并没有直接告诉我们。 相反,你必须根据是否有其他人向该人员报告来推断这一事实。 假设该模型有此属性:

public IList<Employee> DirectReports
{
    get
    {
        ...
    }
}

如果列表为空,则可以推断此 Employee 不是主管。 在这种情况下,EmployeeViewModel 包括提供该逻辑的属性 IsSupervisor

public bool IsSupervisor => _model.DirectReports.Any();

使用 ViewModel 丰富数据

有时,模型可能只提供相关数据的 ID。 或者,可能需要转到多个模型类,才能关联单个屏幕所需的数据。 ViewModel 还为执行这些任务提供理想的位置。 假设需要显示某个员工当前正在管理的所有项目。 此数据不属于 Employee 模型类。 可通过查看 CompanyProjects 访问该数据。 同样,EmployeeViewModel 会将其工作公开为公共属性:

public IEnumerable<string> ActiveProjects => CompanyProjects.All
    .Where(p => p.Owner == _model.Id && p.IsActive)
    .Select(p => p.Name);

使用 ViewModel 传递属性

通常,ViewModel 需要的正好就是模型提供的属性。 对于这些属性,视图模型只是传递数据:

public string Name
{
    get => _model.Name;
    set => _model.Name = value;
}

设置 viewmodel 的范围

可以在任何有视图的级别使用 ViewModel。 页面通常具有 ViewModel,页面的子视图可能也会有。 出现嵌套视图模型的一个常见原因就是页面上显示 ListView。 该列表有一个表示集合的视图模型,例如 EmployeeListViewModel。 列表中的每个元素都是一个 EmployeeViewModel

图中显示了包含多个 EmployeeViewModel 子对象的 EmployeeListViewModel。

还有一种常见的情况,就是有一个保存整个应用程序的数据和状态的顶级 ViewModel,但是它不与任何特定页面关联。 此 ViewModel 通常用于维护“活动”项。 请考虑我们刚才描述的 ListView 示例。 在用户选择员工行时,该员工就表示当前项。 如果用户导航到详细页面,或者在选中该行时选择工具栏按钮,那么执行的操作和显示内容都应该是针对此员工的。 处理这种情况的简洁方式是,将 ListView.SelectItem 数据绑定到工具栏或详细页面也能够访问的属性。 将该属性放在中央 viewmodel 上很有用。

确定何时重用视图模型和视图

如何定义 ViewModel 和模型之间的关系以及 ViewModel 和视图之间的关系,更大程度上由应用要求而非规则决定。 ViewModel 用于为视图提供其所需的结构和数据。 此目的应指导关于 ViewModel 范围“有多大”的决策。

ViewModel 通常会密切反映出模型类的结构,并与该类建立一对一关系。 你之前已看过 EmployeeViewModel 的示例,其中对单个 Employee 实例进行了包装和扩充。 但并非总是建立一对一的关系。 如果将视图模型设计为提供视图需要的内容,可能最终会得到类似 HRDashboardViewModel 的内容来概述 HR 部门,它与任何模型都没有显式关系,但可以使用任何模型类中的数据

同样,你可能会发现 ViewModel 和视图之间通常存在一对一关系。 但并非总是如此。 再想象一下为每个雇员显示一行信息的 ListView。 选择其中一行时,会转至某个雇员的详细页面。

列表页面的 viewmodel 带有一个集合。 如前面所述,集合可以是 EmployeeViewModel 对象的集合。 在用户选择某一行时,EmployeeViewModel 实例可能传递至 EmployeeDetailPage。 详细页面可能将该 EmployeeViewModel 用作它的 BindingContext

这种情况可能是重用 ViewModel 的绝佳时机。 但请记住,ViewModel 旨在提供视图所需的内容。 在某些情况下,可能需要使用彼此独立的 ViewModel,即使它们在相同的模型类基础上构建。 在此示例中,ListView 行很可能比完整的详细信息页需要更少的信息。 如果从详细信息页检索所需的数据会增加过多开销,可能需要同时使用 EmployeeListRowViewModelEmployeeDetailViewModel 模型来提供其各自的视图。

视图模型对象模型

使用实现 INotifyPropertyChanged 的基类意味着无需在每个视图模型上重新实现接口。 请考虑 HR 应用程序,如本培训模块的上一部分所述。 EmployeeViewModel 类实现了 INotifyPropertyChanged 接口,并提供了一个名为 OnPropertyChanged 的帮助程序方法来引发 PropertyChanged 事件。 项目中的其他视图模型(如描述分配给员工的资源的视图模型)还需要 INotifyPropertyChanged 才能与视图完全集成。

MVVM 工具包库(.NET 社区工具包的一部分)是标准、自包含、轻量类型的集合,提供用于使用 MVVM 模式构建新式应用的开始实现。

无需编写自己的视图模型基类,而是继承自工具包的 ObservableObject 类,该类提供视图模型基类所需的一切。 可以通过以下方法简化 EmployeeViewModel

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));
}

更改为以下代码:

using Microsoft.Toolkit.Mvvm.ComponentModel;

public class EmployeeViewModel : ObservableObject
{
    private string _name;

    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
}

可以使用 MVVM 工具包提供的源生成器进一步简化代码。 通过将类设为 partial 并向 private 变量添加 [ObservableProperty],将生成带有适当属性更改通知的公共属性 Name

using Microsoft.Toolkit.Mvvm.ComponentModel;

public partial class EmployeeViewModel : ObservableObject
{
    [ObservableProperty]
    private string _name;
}

MVVM 工具包通过 CommunityToolkit.Mvvm NuGet 包分发。