ObservableObject

ObservableObject 是通过实现 INotifyPropertyChangedINotifyPropertyChanging 接口可观察的对象的基类。 它可用作需要支持属性更改通知的各种对象的起点。

平台 API:ObservableObjectTaskNotifierTaskNotifier<T>

工作原理

ObservableObject 包含以下主要功能:

  • 它提供了对 INotifyPropertyChangedINotifyPropertyChanging 的基本实现,从而公开 PropertyChangedPropertyChanging 事件。
  • 它提供了一系列 SetProperty 方法,这些方法可用于轻松设置继承自 ObservableObject 的类型中的属性值,并自动引发相应的事件。
  • 它提供了 SetPropertyAndNotifyOnCompletion 方法,该方法与 SetProperty 类似,但能够设置 Task 属性并在分配的任务完成后自动引发通知事件。
  • 它公开了 OnPropertyChangedOnPropertyChanging 方法,这些方法可在派生类型中重写,以自定义引发通知事件的方式。

简单属性

下面是如何实现自定义属性的通知支持的示例:

public class User : ObservableObject
{
    private string name;

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

提供的 SetProperty<T>(ref T, T, string) 方法检查属性的当前值,如果不同,则对其进行更新,然后还会自动引发相关事件。 属性名称是通过使用 [CallerMemberName] 属性自动捕获的,因此无需手动指定要更新的属性。

包装不可观察的模型

例如,使用数据库项时,常见方案是创建一个包装“可绑定”模型,该模型中继数据库模型的属性,并在需要时引发属性更改通知。 如果想要向未实现 INotifyPropertyChanged 接口的模型注入通知支持,也需要这样做。 ObservableObject 提供了一种专用方法,使此过程更简单。 在以下示例中,User 是一个直接映射数据库表的模型,不继承自 ObservableObject

public class ObservableUser : ObservableObject
{
    private readonly User user;

    public ObservableUser(User user) => this.user = user;

    public string Name
    {
        get => user.Name;
        set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
    }
}

在本例中,我们将使用 SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string) 重载。 签名比上一个签名要复杂一些,之所以需要这样,是为了让代码仍保持非常高效,即使我们像在上一个方案中那样无权访问支持字段,也是如此。 我们可以详细介绍此方法签名的每个部分,以了解不同组件的角色:

  • TModel 是一个类型参数,指示要包装的模型的类型。 在本例中,它将是 User 类。 请注意,我们不需要显式指定这点 - C# 编译器会通过我们调用 SetProperty 方法的方式自动推断出来。
  • T 是要设置的属性的类型。 与 TModel 类似,它会被自动推断出来。
  • T oldValue 是第一个参数,在本例中,我们将使用 user.Name 传递要包装的该属性的当前值。
  • T newValue 是要对属性设置的新值,此处我们将传递 value,这是属性 setter 中的输入值。
  • TModel model 是我们正在包装的目标模型,在本例中,我们将传递存储在 user 字段中的实例。
  • Action<TModel, T> callback 是一个函数,如果属性的新值不同于当前值,并且需要设置该属性,则会调用该函数。 此操作将由此回调函数完成,该函数接收目标模型和要设置的新属性值作为输入。 在本例中,我们只是(通过执行 u.Name = n)将输入值(我们称为 n)分配给 Name 属性。 在这里,请务必避免从当前范围捕获值,只与作为回调输入的值进行交互,因为这允许 C# 编译器缓存回调函数并执行多项性能改进。 正因为如此,我们才没有直接访问此处的 user 字段或 setter 中的 value 参数,而是只使用 lambda 表达式的输入参数。

SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string) 方法使创建这些包装属性非常简单,因为它负责检索和设置目标属性,同时提供极其紧凑 API。

注意

与使用 LINQ 表达式实现此方法相比,特别是通过类型 Expression<Func<T>> 的参数而不是状态和回调参数实现,可以通过这种方式实现的性能改进非常重要。 具体而言,此版本比使用 LINQ 表达式的版本快约 200 倍,并且根本不进行任何内存分配。

处理 Task<T> 属性

如果属性是一个 Task,则还需要在任务完成后引发通知事件,以便在合适的时间更新绑定以实现某些目的,例如显示与任务所表示的操作有关的加载指示器或其他状态信息。 ObservableObject 具有适用于此方案的 API:

public class MyModel : ObservableObject
{
    private TaskNotifier<int>? requestTask;

    public Task<int>? RequestTask
    {
        get => requestTask;
        set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
    }

    public void RequestValue()
    {
        RequestTask = WebService.LoadMyValueAsync();
    }
}

在这里,SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>, Task<T>, string) 方法将负责更新目标字段、监视新任务(如果存在)并在该任务完成时引发通知事件。 这样,就可以只绑定到任务属性,并在其状态发生更改时收到通知。 TaskNotifier<T> 是一种由 ObservableObject 公开的特殊类型,它包装目标 Task<T> 实例,并为此方法启用必要的通知逻辑。 如果只有常规 Task,也可以直接使用 TaskNotifier 类型。

注意

SetPropertyAndNotifyOnCompletion 方法旨在取代 Microsoft.Toolkit 包中 NotifyTaskCompletion<T> 类型的使用。 如果使用了此类型,则可以将其替换为内部 Task (或 Task<TResult>)属性,然后可以使用 SetPropertyAndNotifyOnCompletion 方法设置其值并引发通知更改。 NotifyTaskCompletion<T> 类型公开的所有属性可直接用于 Task 实例。

示例

  • 查看示例应用(适用于多个 UI 框架),以了解 MVVM 工具包的实际运行情况。
  • 还可以在单元测试中查找更多示例。