ObservableObject
ObservableObject
是通过实现 INotifyPropertyChanged
和 INotifyPropertyChanging
接口可观察的对象的基类。 它可用作需要支持属性更改通知的各种对象的起点。
工作原理
ObservableObject
包含以下主要功能:
- 它提供了对
INotifyPropertyChanged
和INotifyPropertyChanging
的基本实现,从而公开PropertyChanged
和PropertyChanging
事件。 - 它提供了一系列
SetProperty
方法,这些方法可用于轻松设置继承自ObservableObject
的类型中的属性值,并自动引发相应的事件。 - 它提供了
SetPropertyAndNotifyOnCompletion
方法,该方法与SetProperty
类似,但能够设置Task
属性并在分配的任务完成后自动引发通知事件。 - 它公开了
OnPropertyChanged
和OnPropertyChanging
方法,这些方法可在派生类型中重写,以自定义引发通知事件的方式。
简单属性
下面是如何实现自定义属性的通知支持的示例:
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
实例。