一、前言
这一节在文档中是属于MVVM-组件模型里的一个分支,
但是它的上级条目并没有信息,所以直接来看它的内容吧。
二、ObservableObject
ObservableObject是一个基类,通过实现INotifyPropertyChanged和INotifyPropertyChanging接口可以使它的对象可被监视。它可以作为所有需要支持属性更改通知功能的对象的起点。这句话很拗口,因为是直译的,同时也很重要。我来解读一下,MVVM模式中,哪个部分是需要支持属性更改通知的功能的?显然不是View,那是ViewModel还是Model呢?如果Model通知了View,那不如叫MV模式好了,还需要ViewModel做什么?所以这句话意思是它是ViewModel部分中重要的成员。可以说,每个View有对应的ViewModel,而ViewModel都要继承这个类来使得属性能得以暴露给View。
2.1. 它是怎么起作用的
知道了ObservableObject的作用之后,我们来看看它是通过什么方式来让程序员可以实现各种功能的。
ObservableObject 有着以下主要特性:
- 提供了INotifyPropertyChanged 和INotifyPropertyChanging的基本实现,暴露了PropertyChanged 和PropertyChanging事件。
- 提供一系列SetProperty方法,能轻松地设置继承于ObservableObject的类型的属性值,并且自动激发相应的事件。
- 提供了SetPropertyAndNotifyOnCompletion方法,该方法与SetProperty类似,但可以设置Task属性,并在分配任务完成时自动激发通知事件。
- 暴露了OnPropertyChanged 和OnPropertyChanging 方法,这些方法能在派生类中被重写,以自定义如何激发通知事件。
2.2. 简单属性的通知
下面有一个如何实现支持一个自定义属性的通知的例子:
public class User : ObservableObject
{
private string name;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
}
它提供了SetProperty(ref T, T, string)方法来检查当前属性的值,并更新它是否变化,接着会自动激发相关事件。属性名通过使用[CallerMemberName]属性自动会被捕捉,所以并不需要手动指定要更新的属性。
原生的写法应该是这样,从代码量上来看有所简化,MVVM工具包的优点是属性名会自动捕捉。
public class A : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Type _p;
public Type P
{
get => _p;
set
{
_p = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(nameof(P)));
}
}
}
2.3. 封装一个不具备监视功能的模型
在一般场景下,例如,当处理数据库条目时,创建一个封装的可绑定的模型,该模型会传递数据库模型的属性,并在需要时激发属性的变化通知。
有些特殊场景,这样的功能也是被需要的,当想要将支持通知的功能注入到没有实现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的方式自动的推测出类型。很显然,例子中我们填的是user对象而不是User类型。
- T是想要设置的属性的类型。和TModel类似,它也会被自动推断出来。
- T oldValue指的是第一个参数,本例中我们用的是user.Name来传递我们要封装的属性的当前值。(类似于原生set中的 p = value的p)
- T newValue是我们要设置属性的新的值,在这儿我们传了value进去,它是属性setter中的输入值。
- TModel model是我们要封装的目标模型,本例中我们传了user。
- Action<TModel, T> callback是一个方法,若属性的新值与当前不同,该方法就会被调用,并且属性会被设置。这些功能将有这个回调函数来完成,它将目标模型和新的属性值作为输入。在本例中,我们分配了一个叫做n的输入值给Name属性(通过u.Name= n来赋值)。这儿有几点非常重要,就是要避免从当前作用域中获取值,并且只与作为回调输入的值进行交互,因为这可以让C#编译器缓存回调函数并执行一些性能优化。正因为如此,我们不是直接访问这里的user字段或setter的value值,而是只使用lambda表达式中的输入参数。
SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string)方法使得创建封装属性格外简单,因为它负责检索并设置目标属性,同时提供了一个紧凑的API(指的这个API内部帮我们做了大量工作,我们只需要填几个参数就好,从代码量上来看确实极简,但从个人角度来讲,这种多参数的API我不是很喜欢)。
Note:
与用LINQ表达式实现该方法相比,具体指的是通过Expression<Func<T>> 代替状态和回调参数,这种方式的性能改进非常显著。
特别地,这个版本比使用LINQ表达式的版本快了约200倍,并且没有用到任何的内存分配。
2.4. 处理Task<T>属性
如果要用在Task类型的属性上,在Task完成时激发通知事件以便适时更新绑定(Binding,也可叫关联)。例如,在其它任务操作时,显示一个加载条或其他的状态信息。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>实例,为该方法启用必要的通知逻辑。如果你只有一个普通的任务,TaskNotifier类型也可以直接使用。
Note:
SetPropertyAndNotifyOnCompletion 是被用来替换Microsoft.Toolkit包中的NotifyTaskCompletion<T>类型的。
如果该类型正在被使用,它可以用内部的Task(或Task<TResult>)属性替换,接着SetPropertyAndNotifyOnCompletion
能被用于设置它的值并且激发通知改变。
在Task实例中,所有由NotifyTaskCompletion<T>暴露的属性都是直接可用的。
三、小结
这节内容在WPF原生的MVVM中也不难实现,更多的是熟悉MVVM工具包的里的写法。