CommunityToolkit.Mvvm 加速 MVVM 开发
- CommunityToolkit.Mvvm 简介
- CommunityToolkit.Mvvm 包含的实现
- 源生成器
- 不使用源生成器
- 使用源生成器
- ObservableProperty 属性
- 使用方式
- 通知依赖属性
- 通知依赖命令
- 请求属性验证
- 发送通知消息
- RelayCommand 属性
- 使用方式
- 命令参数
- 异步命令
- 启用和禁用命令
- 处理并发执行
- 处理异步异常
- 取消异步操作的命令
- INotifyPropertyChanged 属性
- 使用方式
- 可观测对象
- ObservableObject
- 使用方式
- 包装不可观测模型
- 处理 Task<T> 属性
- ObservableRecipient
- ObservableValidator
- 示例
- 自定义验证方法
- 自定义验证属性
- 命令
- RelayCommand 和 RelayCommandT<>
- 使用 ICommand
- AsyncRelayCommand 和 AsyncRelayCommand<T>
- 使用 IAsyncRelayCommand
- 依赖注入
- 配置服务和解析
- 构造函数注入
- 数据上下文的分配
- Messenger
- 发送和接收消息
- 使用请求消息
- 结语
为保文字描述的准确性,文章中的文字描述信息多为引用官方。
依赖注入部分按照官方文档的步骤,但是替换成了我自己写的WPF的内容,因为官方给的是UWP这块不熟悉怕出错就替换了。
CommunityToolkit.Mvvm 简介
- 引用 Microsoft Document 中的介绍
包
CommunityToolkit.Mvvm
(又名 MVVM 工具包,以前名为Microsoft.Toolkit.Mvvm
) 是一个现代、快速且模块化的 MVVM 库。 它是 .NET 社区工具包的一部分,围绕以下原则构建:
- 平台和运行时独立 - .NET Standard 2.0、 .NET Standard 2.1 和 .NET 6🚀 (UI Framework 不可知)
- 易于选取和使用 - 在“MVVM”) 之外,对应用程序结构或编码范例 (没有严格的要求,即灵活使用。
- 点菜 - 自由选择要使用的组件。
- 参考实现 - 精益和性能,为基类库中包含的接口提供实现,但缺少直接使用它们的具体类型。
MVVM 工具包由 Microsoft 维护和发布,是 .NET Foundation 的一部分。 它还由内置于 Windows 中的多个第一方应用程序(例如 Microsoft Store)使用。
此包面向 .NET Standard,因此可在任何应用平台上使用:UWP、WinForms、WPF、Xamarin、Uno 等;和在任何运行时上:.NET Native、.NET Core、.NET Framework或 Mono。 它在所有它们上运行。 API 图面在所有情况下都是相同的,因此非常适合生成共享库。
此外,MVVM 工具包还有 一个 .NET 6 目标,用于在 .NET 6 上运行时启用更多内部优化。 在这两种情况下,公共 API 图面完全相同,因此 NuGet 将始终解析包的最佳版本,而使用者无需担心哪些 API 将在其平台上可用。
CommunityToolkit.Mvvm 包含的实现
- 源生成器
- 可观测对象
- 命令
- 依赖注入
- Messenger
CommunityToolkit.Mvvm
相较于 Prism
、MvvmCross
等其他 MVVM开发框架是很轻量的。
本系列文章将根据上述的实现通过一个 WPF 案例来探讨使用 CommunityToolkit.Mvvm
包对于 MVVM 开发的优势所在。
源生成器
从版本 8.0 开始,MVVM 工具包包含全新的
Roslyn
源生成器,有助于在使用 MVVM 体系结构编写代码时大幅减少样板。 它们可简化需要设置可观察属性、命令等的方案。 如果不熟悉源生成器,可 在此处 阅读有关它们的详细信息。
这意味着,在编写代码时,MVVM 工具包生成器现在将负责在后台为你生成其他代码,因此无需担心。 然后,此代码将编译并包含在应用程序中,因此最终结果与手动编写所有额外代码完全相同,但不必执行所有这些额外工作! 🎉。
不使用源生成器
private string _title;
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
private ICommand? HelloCommand;
public Test()
{
Title = "Hello";
HelloCommand = new RelayCommand(Hello);
}
public void Hello()
{
Console.WriteLine("Hello");
}
通过上面的代码不难看出,无论是将属性标记为可观测对象,还是声明 command 都需要编写大量且重复的代码,这对于快速开发来讲是非常不利的。常用的简化代码的手段是通过标记
attribute
使用反射的方式,但是这种方式需要在前期编写大量的代码以实现该功能,劝退很多人。
使用源生成器
[ObservableProperty]
public string _name;
[RelayCommand]
public void HelloName()
{
Console.WriteLine("Hello");
}
8.0 版本之后通过
Roslyn
源生成器即可自动转换代码
这里需要注意的是:
Property 的声明需要使用驼峰式命名法命名属性名
当前类需要标记为 部分类partial
原生成器在一定程度上降低了重复代码的编写从而使开发进度加快。
源生成器可以独立于 MVVM 工具包中的其他现有功能使用 因为 源生成器是由.NET Compiler Platform(“Roslyn”)SDK 附带 所以并不依赖某个工具包。
- MVVM 生成器中包含的功能:
- CommunityToolkit.Mvvm.ComponentModel
- ObservableProperty
- INotifyPropertyChanged
- CommunityToolkit.Mvvm.Input
- RelayCommand
- CommunityToolkit.Mvvm.ComponentModel
ObservableProperty 属性
- 类型
ObservableProperty
是一个属性,允许从批注字段生成可观察属性。 其用途是大大减少定义可观测属性所需的样本量。
注意:
若要正常工作,批注字段需要位于具有必要INotifyPropertyChanged
基础结构的 分部类 中。 如果该类型是嵌套的,则声明语法树中的所有类型也必须注释为部分。 否则将导致编译错误,因为生成器将无法使用请求的可观测属性生成该类型的不同分部声明。
使用方式
[ObservableProperty]
private string? name;
将生成如下等效的可观测对象:
public string? Name
{
get => name;
set => SetProperty(ref name, value);
}
注意:
将基于字段名称创建生成的属性的名称。 生成器假定字段命名lowerCamel
为 、_lowerCamel
或m_lowerCamel
,并将转换为UpperCamel
,以遵循正确的 .NET 命名约定。 生成的属性将始终具有公共访问器,但可以使用任何可见性声明字段, (private
建议) 。
通知依赖属性
假设有一个属性
FullName
想要在更改 Name 时发出通知的属性。 可以使用 属性执行此操作NotifyPropertyChangedFor
。
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? name;
将生成如下等效的属性
public string? Name
{
get => name;
set
{
if (SetProperty(ref name, value))
{
OnPropertyChanged("FullName");
}
}
}
通知依赖命令
假设有一个命令
MyCommand
,其执行状态取决于此属性的值。 也就是说,每当属性更改时,命令的执行状态都应失效并再次计算。 换句话说,ICommand.CanExecuteChanged
应再次引发 。 可以使用 属性来实现此目的NotifyCanExecuteChangedFor
。
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MyCommand))]
private string? name;
将生成如下等效的属性
public string? Name
{
get => name;
set
{
if (SetProperty(ref name, value))
{
MyCommand.NotifyCanExecuteChanged();
}
}
}
请求属性验证
如果属性是在继承自
ObservableValidator
的类型中声明的,则还可以使用任何验证属性对其进行批注,然后请求生成的setter
触发该属性的验证。 这可以通过 属性来实现NotifyDataErrorInfo
。
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2)] // Any other validation attributes too...
private string? name;
将生成如下等效的属性
public string? Name
{
get => name;
set
{
if (SetProperty(ref name, value))
{
ValidateProperty(value, "Value2");
}
}
}
生成的
ValidateProperty
调用将验证 属性并更新对象的状态ObservableValidator
,以便 UI 组件可以对其进行响应并相应地显示任何验证错误。
注意:
根据设计,只有继承自ValidationAttribute
的字段属性才会转发到生成的属性。 这是专门为支持数据验证方案而进行的。 将忽略所有其他字段属性,因此目前无法在字段上添加其他自定义属性,并将它们也应用于生成的属性。 如果需要, (例如若要控制序列化) ,请考虑改用传统的手动属性。
发送通知消息
如果属性是在继承自
ObservableRecipient
的类型中声明的,则可以使用NotifyPropertyChangedRecipients
特性指示生成器也插入代码,以便为属性更改发送属性更改消息。 这将允许已注册的收件人动态响应更改。
[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string? name;
将生成如下等效的属性
public string? Name
{
get => name;
set
{
string? oldValue = name;
if (SetProperty(ref name, value))
{
Broadcast(oldValue, value);
}
}
}
生成的
Broadcast
调用将使用当前 viewmodel 中使用的 实例向所有已注册的订阅者发送新的 PropertyChangedMessageIMessenger
。
RelayCommand 属性
类型
RelayCommand
是一个属性,允许为带批注的方法生成中继命令属性。 其用途是完全消除在 viewmodel 中定义包装专用方法的命令所需的样本。
注意:
若要正常工作,批注方法需要位于 分部类 中。 如果类型是嵌套的,则声明语法树中的所有类型也必须注释为部分。 这样做将导致编译错误,因为生成器将无法使用请求的命令生成该类型的不同部分声明。
使用方式
- 该
RelayCommand
特性可用于批注分部类型中的方法
[RelayCommand]
private void GreetUser()
{
Console.WriteLine("Hello!");
}
将生成如下等效命令
private RelayCommand? greetUserCommand;
public IRelayCommand GreetUserCommand => greetUserCommand ??= new RelayCommand(GreetUser);
将基于方法名称创建生成的命令的名称。 生成器将使用方法名称并在末尾追加 “Command”,如果存在,它将去除 “On” 前缀。 此外,对于异步方法,在应用 “Command” 之前,也会删除 “Async” 后缀。
命令参数
该
[RelayCommand]
属性支持使用参数为方法创建命令。 在这种情况下,它会自动将生成的命令更改为IRelayCommand<T>
相反,接受相同类型的参数。
[RelayCommand]
private void GreetUser(User user)
{
Console.WriteLine($"Hello {user.Name}!");
}
将生成如下等效代码
private RelayCommand<User>? greetUserCommand;
public IRelayCommand<User> GreetUserCommand => greetUserCommand ??= new RelayCommand<User>(GreetUser);
- 生成的命令将自动使用参数的类型作为其类型参数
异步命令
该
[RelayCommand]
命令还支持通过IAsyncRelayCommand
接口IAsyncRelayCommand<T>
包装异步方法。 每当方法返回Task
类型时,都会自动处理此情况。
[RelayCommand]
private async Task GreetUserAsync()
{
User user = await userService.GetCurrentUserAsync();
Console.WriteLine($"Hello {user.Name}!");
}
将生成如下等效代码
private AsyncRelayCommand? greetUserCommand;
public IAsyncRelayCommand GreetUserCommand => greetUserCommand ??= new AsyncRelayCommand(GreetUserAsync);
- 如果该方法采用参数,则生成的命令也将是泛型命令。
此方法具有
CancellationToken
一个特殊情况,因为该方法将传播到命令以启用取消。 也就是说,如下所示的方法:
[RelayCommand]
private async Task GreetUserAsync(CancellationToken token)
{
try
{
User user = await userService.GetCurrentUserAsync(token);
Console.WriteLine($"Hello {user.Name}!");
}
catch (OperationCanceledException)
{
}
}
将导致生成的命令将令牌传递给包装方法。 这使使用者只需调用 IAsyncRelayCommand.Cancel 以发出该令牌的信号,并允许挂起的操作正确停止。
启用和禁用命令
通常,能够禁用命令,然后才能使状态失效,并再次检查是否可以执行命令。 为了支持这一点,该
RelayCommand
属性公开CanExecute
属性,该属性可用于指示用于评估是否可以执行命令的目标属性或方法
[RelayCommand(CanExecute = nameof(CanGreetUser))]
private void GreetUser(User? user)
{
Console.WriteLine($"Hello {user!.Name}!");
}
private bool CanGreetUser(User? user)
{
return user is not null;
}
这样,
CanGreetUser
当按钮首次绑定到 UI ((例如,按钮) )时调用,然后在每次IRelayCommand.NotifyCanExecuteChanged
调用命令时再次调用该按钮。
例如,这是命令可以绑定到属性以控制其状态的方式:
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(GreetUserCommand))]
private User? selectedUser;
<!-- Note: this example uses traditional XAML binding syntax -->
<Button
Content="Greet user"
Command="{Binding GreetUserCommand}"
CommandParameter="{Binding SelectedUser}"/>
每次生成
SelectedUser
属性的值更改时都会调用GreetUserCommand.NotifyCanExecuteChanged()
方法。 UI 具有Button
控件绑定GreetUserCommand
,这意味着每次引发其CanExecuteChanged
事件时,它都会再次调用其CanExecute
方法。 这将导致计算包装CanGreetUser
的方法,这将基于输入实例 (SelectedUser
UI 中绑定到属性)null
是否User
返回按钮的新状态。 这意味着每当SelectedUser
发生更改时,GreetUserCommand
都将基于该属性是否具有值(此方案中所需的行为)启用。
注意:
当方法或属性的CanExecute
返回值发生更改时,该命令不会自动知道。 由开发人员调用IRelayCommand.NotifyCanExecuteChanged
使命令失效,并请求再次评估链接CanExecute
方法,然后更新绑定到命令的控件的视觉状态。
处理并发执行
每当命令是异步的,都可以将其配置为决定是否允许并发执行。 使用特性
RelayCommand
时,可以通过属性设置此属性AllowConcurrentExecutions
。 默认值为false
,这意味着在执行挂起之前,该命令会将其状态指示为禁用状态。 如果设置为true
该调用,则可以将任意数量的并发调用排队。
注意:
如果命令接受取消令牌,则请求并发执行时也会取消令牌。 主要区别是,如果允许并发执行,该命令将保持启用状态,并且它将启动新的请求执行,而无需等待上一个执行实际完成。
处理异步异常
异步中继命令处理异常的方式有两种不同的方法:
- await 和重新引发 (默认) :当命令等待调用完成时,任何异常自然都会在同一同步上下文中引发。 这通常意味着引发的异常只会使应用崩溃,这与同步命令的行为一致, (引发异常也会使应用崩溃) 。
- 流异常到任务计划程序:如果命令配置为将异常流式流式传送到任务计划程序,则引发的异常不会使应用崩溃,而是通过公开
IAsyncRelayCommand.ExecutionTask
的异常以及浮泡TaskScheduler.UnobservedTaskException
而变为可用。 这可实现更高级的方案 (,例如,让 UI 组件绑定到任务,并根据操作结果) 显示不同的结果,但正确使用更为复杂。
默认行为是命令等待并重新引发异常。 这可以通过属性进行配置 FlowExceptionsToTaskScheduler
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task GreetUserAsync(CancellationToken token)
{
User user = await userService.GetCurrentUserAsync(token);
Console.WriteLine($"Hello {user.Name}!");
}
在这种情况下, try/catch 不需要,因为异常将不再崩溃应用。
注意:
这还会导致其他不相关的异常自动重新引发,因此应仔细决定如何处理每个单独的方案并相应地配置其余代码。
取消异步操作的命令
异步命令的最后一个选项是请求生成取消命令的功能。 这是一个
ICommand
包装异步中继命令,可用于请求取消操作。 此命令将自动发出信号,以反映它是否可以在任何给定时间使用。 例如,如果链接命令未执行,它将报告其状态,因为它不可执行。
[RelayCommand(IncludeCancelCommand = true)]
private async Task DoWorkAsync(CancellationToken token)
{
// Do some long running work...
}
这将导致 DoWorkCancelCommand
也会生成属性。 然后,可以绑定到其他一些 UI 组件,以便用户轻松取消挂起的异步操作。
INotifyPropertyChanged 属性
该
INotifyPropertyChanged
类型是一个属性,允许将 MVVM 支持代码插入现有类型。 与其他相关属性 (ObservableObject
和ObservableRecipient
) 一起,其用途是支持开发人员,以防需要这些类型的相同功能,但目标类型已经从另一种类型实现。 由于 C# 不允许多个继承,因此这些属性可用于让 MVVM 工具包生成器将相同的代码直接添加到这些类型中,从而避开此限制。
注意:
若要正常工作,批注类型需要位于 分部类 中。 如果类型是嵌套的,则声明语法树中的所有类型也必须注释为部分。 这样做将导致编译错误,因为生成器将无法使用请求的其他代码生成该类型的不同部分声明。
这些属性仅用于目标类型不能仅从等效类型继承 (等情况下使用。从ObservableObject
) 。 如果可能,则继承是建议的方法,因为它通过将重复的代码创建到最终程序集中来减小二进制大小。
使用方式
使用这些属性中的任何一个非常简单:只需将它们添加到 分部类 ,相应的类型中的所有代码都将自动生成到该类型中。
[INotifyPropertyChanged]
public partial class MyViewModel : SomeOtherType
{
}
这将在类型中 MyViewModel
生成完整的 INotifyPropertyChanged
实现,并完成其他帮助程序 (,例如SetProperty
可用于减少详细程度) 。 下面是各种属性的简要摘要:
- INotifyPropertyChanged:实现接口并添加帮助程序方法来设置属性并引发事件。
- ObservableObject:添加类型中的所有代码
ObservableObject
。 从概念上讲,它与INotifyPropertyChanged
它实现的主要区别是, 它也是实现INotifyPropertyChanging
的。 - ObservableRecipient:添加类型中的所有代码
ObservableRecipient
。 具体而言,这可以添加到从ObservableValidator
中继承的类型来合并这两种类型。
可观测对象
ObservableObject
ObservableObject
这是通过实现INotifyPropertyChanged
和INotifyPropertyChanging
接口可观察的对象的基类。 它可以用作需要支持属性更改通知的各种对象的起点。
ObservableObject
具有以下主要功能:- 它为和
INotifyPropertyChanging
公开PropertyChanged
事件PropertyChanging
提供了基本实现INotifyPropertyChanged
。 - 它提供了一系列
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
) ,将输入值 (nName
分配给属性 () 。 在这里,请务必避免从当前范围捕获值,并且只与作为回调输入的值进行交互,因为这允许 C# 编译器缓存回调函数并执行许多性能改进。 这是因为,我们不只是直接访问 user 此处的字段或value
setter 中的参数,而是只使用 lambda 表达式的输入参数。
该方法
SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string)
使创建这些包装属性极其简单,因为它负责检索和设置目标属性,同时提供极其紧凑 API。
注意:
与使用 LINQ 表达式的此方法的实现相比,特别是通过类型Expression<Func<T>>
参数而不是状态和回调参数实现,可以通过此方法实现的性能改进非常重要。 具体而言,此版本比使用 LINQ 表达式快约 200 倍,根本不会进行任何内存分配。
处理 Task 属性
如果属性是一个
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>
实例并启用此方法所需的通知逻辑。 TaskNotifier 如果只有常规Task
类型,还可以直接使用该类型。
注意:
该方法SetPropertyAndNotifyOnCompletion
旨在替换包中Microsoft.Toolkit
类型的用法NotifyTaskCompletion<T>
。 如果使用了此类型,则只能将其替换为内部Task
(或Task<TResult>
) 属性,然后SetPropertyAndNotifyOnCompletion
该方法可用于设置其值并引发通知更改。 类型公开NotifyTaskCompletion<T>
的所有属性都直接在实例上 Task 可用。
ObservableRecipient
该
ObservableRecipient
类型是可观察对象的基类,也充当邮件的收件人。 此类是一个扩展ObservableObject
,它还提供使用类型的IMessenger
内置支持。
-
该
ObservableRecipient
类型旨在用作也使用该IMessenger
功能的 viewmodel 的基础,因为它为它提供内置支持。 具体而言:- 它有一个无参数构造函数和一个
IMessenger
用于依赖注入实例的构造函数。 它还公开Messenger
可用于在 viewmodel 中发送和接收消息的属性。 如果使用无参数构造函数,则会WeakReferenceMessenger.Default
将实例分配给Messenger
该属性。 - 它公开用于
IsActive
激活/停用 viewmodel 的属性。 在此上下文中,若要"激活",则表示给定的 viewmodel 被标记为正在使用,例如。它将开始侦听已注册的消息、执行其他设置操作等。有两个相关方法,OnActivated
在OnDeactivated
属性更改值时调用该方法。 默认情况下,OnDeactivated
自动从所有已注册的消息中注销当前实例。 为了获得最佳结果并避免内存泄漏,建议用于OnActivated
向消息注册以及用于OnDeactivated
执行清理操作。 此模式允许多次启用/禁用 viewmodel,同时在每次停用内存泄漏风险的情况下安全地收集。 默认情况下,OnActivated
将自动注册通过IRecipient<TMessage>
接口定义的所有消息处理程序。 - 它公开一个
Broadcast<T>(T, T, string)
方法,该方法通过IMessenger
属性中可用的Messenger实例发送PropertyChangedMessage<T>
消息。 这可用于轻松广播 viewmodel 属性中的更改,而无需手动检索Messenger
要使用的实例。 此方法由各种 SetProperty 方法的重载使用,该方法具有附加bool broadcast
属性来指示是否也发送消息。
- 它有一个无参数构造函数和一个
-
下面是在活动时接收 LoggedInUserRequestMessage 消息的 viewmodel 示例
public class MyViewModel : ObservableRecipient, IRecipient<LoggedInUserRequestMessage>
{
public void Receive(LoggedInUserRequestMessage message)
{
// Handle the message here
}
}
- 在上面的示例中,
OnActivated
使用该方法作为要调用的操作自动将实例注册为邮件收件人LoggedInUserRequestMessage
。IRecipient<TMessage>
使用接口不是必需的,注册也可以手动 (,即使仅使用内联 lambda 表达式)
public class MyViewModel : ObservableRecipient
{
protected override void OnActivated()
{
// Using a method group...
Messenger.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) => r.Receive(m));
// ...or a lambda expression
Messenger.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
// Handle the message here
});
}
private void Receive(LoggedInUserRequestMessage message)
{
// Handle the message here
}
}
ObservableValidator
ObservableValidator
是实现 接口的INotifyDataErrorInfo
基类,支持验证向其他应用程序模块公开的属性。 它还继承自ObservableObject
,因此它也实现INotifyPropertyChanged
和INotifyPropertyChanging
。 它可以用作需要同时支持属性更改通知和属性验证的所有对象的起点。
ObservableValidator
具有以下主要功能:- 它为 提供基本实现
INotifyDataErrorInfo
,公开ErrorsChanged
事件和其他必要的 API。 - 它提供一系列附加
SetProperty
重载, (基ObservableObject
类) 提供的重载,这些重载提供在更新其值之前自动验证属性和引发必要事件的能力。 - 它公开了许多
TrySetProperty
重载,这些重载类似于SetProperty
,但仅在验证成功时更新目标属性,如果有任何) 进一步检查,则返回生成的错误 (。 - 它公开
ValidateProperty
方法,该方法可用于手动触发特定属性的验证,以防其值尚未更新,但其验证依赖于已更新的另一个属性的值。 - 它公开
ValidateAllProperties
方法,该方法会自动执行当前实例中所有公共实例属性的验证,前提是它们至少应用了一个[ValidationAttribute]
属性。 - 它公开了一个
ClearAllErrors
方法,该方法在重置绑定到用户可能想要再次填充的某个表单的模型时非常有用。 - 它提供了许多构造函数,允许传递不同的参数来初始化
ValidationContext
将用于验证属性的实例。 使用可能需要其他服务或选项才能正常工作的自定义验证属性时,这尤其有用。
- 它为 提供基本实现
示例
实现支持更改通知和验证的属性
public class RegistrationForm : ObservableValidator
{
private string name;
[Required]
[MinLength(2)]
[MaxLength(100)]
public string Name
{
get => name;
set => SetProperty(ref name, value, true);
}
}
此处,我们将调用
SetProperty<T>(ref T, T, bool, string)
由ObservableValidator
公开的方法,并将附加 bool 参数设置为 true 指示我们还希望在属性值更新时验证属性。ObservableValidator
将使用使用应用于 属性的属性指定的所有检查,对每个新值自动运行验证。 然后,其他 (组件(如 UI 控件) )可以与 viewmodel 交互,并修改其状态以反映 viewmodel 中当前存在的错误,方法是注册ErrorsChanged
并使用GetErrors(string)
方法检索已修改的每个属性的错误列表。
自定义验证方法
有时,验证属性需要 viewmodel 有权访问其他服务、数据或其他 API。 可通过不同的方法向属性添加自定义验证,具体取决于方案和所需的灵活性级别。
public class RegistrationForm : ObservableValidator
{
private readonly IFancyService service;
public RegistrationForm(IFancyService service)
{
this.service = service;
}
private string name;
[Required]
[MinLength(2)]
[MaxLength(100)]
[CustomValidation(typeof(RegistrationForm), nameof(ValidateName))]
public string Name
{
get => this.name;
set => SetProperty(ref this.name, value, true);
}
public static ValidationResult ValidateName(string name, ValidationContext context)
{
RegistrationForm instance = (RegistrationForm)context.ObjectInstance;
bool isValid = instance.service.Validate(name);
if (isValid)
{
return ValidationResult.Success;
}
return new("The name was not validated by the fancy service");
}
}
在这种情况下,我们有一个静态
ValidateName
方法,该方法将通过注入到 viewmodel 中的服务对Name
属性执行验证。 此方法接收name
属性值和正在使用的ValidationContext
实例,其中包含诸如 viewmodel 实例、要验证的属性的名称以及服务提供程序和一些可以使用或设置的自定义标志等内容。 在本例中,我们将从验证上下文中检索RegistrationForm
实例,并从那里使用注入的服务来验证 属性。 请注意,此验证将在其他属性中指定的验证旁边执行,因此我们可以随意组合自定义验证方法和现有验证属性,但我们喜欢。
自定义验证属性
执行自定义验证的另一种方法是实现自定义
[ValidationAttribute]
,然后将验证逻辑插入重写IsValid
的方法中。 与上述方法相比,这样可以实现额外的灵活性,因为它使得在多个位置重复使用同一属性变得非常简单。
public sealed class GreaterThanAttribute : ValidationAttribute
{
public GreaterThanAttribute(string propertyName)
{
PropertyName = propertyName;
}
public string PropertyName { get; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
object
instance = validationContext.ObjectInstance,
otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);
if (((IComparable)value).CompareTo(otherValue) > 0)
{
return ValidationResult.Success;
}
return new("The current value is smaller than the other one");
}
}
将此属性添加到 viewmodel 中
public class ComparableModel : ObservableValidator
{
private int a;
[Range(10, 100)]
[GreaterThan(nameof(B))]
public int A
{
get => this.a;
set => SetProperty(ref this.a, value, true);
}
private int b;
[Range(20, 80)]
public int B
{
get => this.b;
set
{
SetProperty(ref this.b, value, true);
ValidateProperty(A, nameof(A));
}
}
}
在这种情况下,我们有两个数字属性,这些属性必须位于特定的范围内,并且彼此之间具有特定关系 (
A
需要大于B
) 。 我们在第一个属性上添加了新的[GreaterThanAttribute]
,并在 的setter
中添加了对ValidateProperty
的B
调用,因此每当B
更改 (时,会再次验证,A
因为其验证状态取决于它) 。 只需在 viewmodel 中使用这两行代码即可启用此自定义验证,并且还获得了一个可重用的自定义验证属性的好处,该属性在应用程序中的其他 viewmodel 中也很有用。 此方法还有助于代码模块化,因为验证逻辑现在与 viewmodel 定义本身完全分离。
命令
RelayCommand 和 RelayCommandT<>
并且
RelayCommandRelayCommand<T>
是ICommand
可以向视图公开方法或委托的实现。 这些类型充当在 viewmodel 和 UI 元素之间绑定命令的方法。
RelayCommand
具有以下RelayCommand<T>
主要功能:- 它们提供接口的基本 ICommand 实现。
- 它们还实现
IRelayCommand
(和IRelayCommand<T>
) 接口,该接口公开了NotifyCanExecuteChanged
引发CanExecuteChanged
事件的方法。 - 它们公开采用类似和(允许包装标准方法和 lambda 表达式)的
ActionFunc<T>
委托的构造函数。
使用 ICommand
public class MyViewModel : ObservableObject
{
public MyViewModel()
{
IncrementCounterCommand = new RelayCommand(IncrementCounter);
}
private int counter;
public int Counter
{
get => counter;
private set => SetProperty(ref counter, value);
}
public ICommand IncrementCounterCommand { get; }
private void IncrementCounter() => Counter++;
}
<Page
x:Class="MyApp.Views.MyPage"
xmlns:viewModels="using:MyApp.ViewModels">
<Page.DataContext>
<viewModels:MyViewModel x:Name="ViewModel"/>
</Page.DataContext>
<StackPanel Spacing="8">
<TextBlock Text="{x:Bind ViewModel.Counter, Mode=OneWay}"/>
<Button
Content="Click me!"
Command="{x:Bind ViewModel.IncrementCounterCommand}"/>
</StackPanel>
</Page>
绑定到
Button
viewmodel 中包装专用IncrementCounter
方法的绑定ICommand
。 显示TextBlock
属性的值Counter
,并在每次属性值更改时更新。
AsyncRelayCommand 和 AsyncRelayCommand
AsyncRelayCommand<T>
以及AsyncRelayCommand
扩展ICommand
由RelayCommand
异步操作提供支持的功能的实现。
AsyncRelayCommand
具有以下AsyncRelayCommand<T>
主要功能:- 它们扩展库中包含的同步命令的功能,并支持 Task返回委托。
- 它们可以使用附加
CancellationToken
参数包装异步函数以支持取消,并公开属性CanBeCanceledIsCancellationRequested
以及Cancel
方法。 - 它们公开可用于
ExecutionTask
监视挂起操作进度的属性,以及IsRunning
可用于检查操作完成时间的属性。 这对于将命令绑定到 UI 元素(如加载指示器)特别有用。 - 它们实现和
IAsyncRelayCommandIAsyncRelayCommand<T>
接口,这意味着 viewmodel 可以使用它们轻松公开命令,以减少类型之间的紧密耦合。 例如,这样就可以轻松地将命令替换为公开相同公共 API 图面的自定义实现(如果需要)。
使用 IAsyncRelayCommand
public class MyViewModel : ObservableObject
{
public MyViewModel()
{
DownloadTextCommand = new AsyncRelayCommand(DownloadText);
}
public IAsyncRelayCommand DownloadTextCommand { get; }
private Task<string> DownloadText()
{
return WebService.LoadMyTextAsync();
}
}
<Page
x:Class="MyApp.Views.MyPage"
xmlns:viewModels="using:MyApp.ViewModels"
xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters">
<Page.DataContext>
<viewModels:MyViewModel x:Name="ViewModel"/>
</Page.DataContext>
<Page.Resources>
<converters:TaskResultConverter x:Key="TaskResultConverter"/>
</Page.Resources>
<StackPanel Spacing="8" xml:space="default">
<TextBlock>
<Run Text="Task status:"/>
<Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask.Status, Mode=OneWay}"/>
<LineBreak/>
<Run Text="Result:"/>
<Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask, Converter={StaticResource TaskResultConverter}, Mode=OneWay}"/>
</TextBlock>
<Button
Content="Click me!"
Command="{x:Bind ViewModel.DownloadTextCommand}"/>
<ProgressRing
HorizontalAlignment="Left"
IsActive="{x:Bind ViewModel.DownloadTextCommand.IsRunning, Mode=OneWay}"/>
</StackPanel>
</Page>
单击该
Button
命令后,将调用命令并ExecutionTask
更新。 操作完成后,该属性将引发 UI 中反映的通知。 在这种情况下,将显示任务状态和任务的当前结果。 请注意,若要显示任务的结果,必须使用TaskExtensions.GetResultOrDefault
该方法 - 这提供了对尚未完成且尚未完成的任务(未阻止线程 (并可能导致死锁) )的访问。
依赖注入
一种常见模式,可用于使用 MVVM 模式提高应用程序的代码库中的模块化性,即使用某种形式的反转控制。 最常见的解决方案之一是使用依赖项注入,它包括创建大量注入后端类的服务, (ie。作为参数传递给 viewmodel 构造函数) - 这允许使用这些服务的代码不依赖于这些服务的实现详细信息,并且还可以轻松地交换这些服务的具体实现。 此模式还使平台特定的功能更易于后端代码使用,方法是通过服务进行抽象化,然后在需要时注入这些功能。
MVVM 工具包不提供内置 API 来方便使用此模式,因为已有专用库专门用于此模式,例如
Microsoft.Extensions.DependencyInjection
包,该包提供功能齐全的强大 DI API 集,并充当易于设置和使用IServiceProvider
的功能。 以下指南将参考此库,并提供一系列示例,说明如何使用 MVVM 模式将其集成到应用程序中。
配置服务和解析
确定已安装 Microsoft.Extensions.DependencyInjection
包
第一步是声明实例 IServiceProvider
,并在启动时初始化所有必要的服务,App.xaml.cs
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public App()
{
Services = ConfigureServices();
this.InitializeComponent();
}
public new static App Current => (App)Application.Current;
public IServiceProvider Services { get; }
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddScoped<BusinessControlView>();
services.AddSingleton<ShellViewModel>();
services.AddScoped<BusinessControlViewModel>();
return services.BuildServiceProvider();
}
}
至此,该
Services
属性在启动时初始化,所有应用程序服务和 viewmodel 都注册。 还有一个新Current
属性可用于从应用程序中的其他视图轻松访问Services
该属性。
- 我这里暂时没有抽象出来的 Interface 就用 view实例来替代 service provide
var view = App.Current.Services.GetService<Views.BusinessControlView>();
构造函数注入
构造函数注入意味着 DI 服务提供商会自动收集所有必要的服务
public partial class ShellViewModel : ViewModelBase
{
private readonly IMyService _myService;
public ShellViewModel(IMyService myService)
{
_myService = myService;
CurrentViewModel = App.Current.Services.GetService<Views.BusinessControlView>()!;
_window = Application.Current.MainWindow;
}
}
数据上下文的分配
public partial class ShellView : Window
{
public ShellView()
{
InitializeComponent();
this.DataContext = App.Current.Services.GetService<ShellViewModel>();
}
}
Messenger
接口
IMessenger
是可用于在不同对象之间交换消息的类型协定。 这可用于将应用程序的不同模块分离,而无需保留对所引用类型的强引用。 还可以将消息发送到特定通道,由令牌唯一标识,并在应用程序的不同部分中具有不同的信使。 MVVM Toolkit 提供两种现装实现:WeakReferenceMessenger
前StrongReferenceMessenger
者在内部使用弱引用,为收件人提供自动内存管理,而后者使用强引用,并且要求开发人员在不再需要接收者时手动取消订阅收件人,但换而言,这样 可以提高性能和少得多的内存 使用。
实现
IMessenger
的类型负责维护接收方与相对消息处理程序) 的邮件 (接收方之间的链接。 任何对象都可以使用消息处理程序将给定邮件类型注册为收件人,每当IMessenger
该实例用于发送该类型的消息时,都会调用该对象。 还可以通过特定通信通道发送消息, (唯一令牌) 标识的每个通道,以便多个模块可以交换同一类型的消息,而不会造成冲突。 在没有令牌的情况下发送的消息使用默认共享通道。
可通过两种方式执行消息注册:通过
IRecipient<TMessage>
接口或使用充当消息处理程序的MessageHandler<TRecipient, TMessage>
委托。 第一个允许使用对扩展的单个调用注册所有处理程序,该扩展RegisterAll
会自动注册所有声明的消息处理程序的收件人,而后者在需要更多灵活性或想要将简单的 lambda 表达式用作消息处理程序时非常有用。
StrongReferenceMessenger
同时WeakReferenceMessenger
公开一个Default
属性,该属性提供内置于包中的线程安全实现。 如果需要,还可以创建多个信使实例,例如,如果将另一个信使实例注入到应用的不同模块中, (应用的不同模块,则在同一进程中运行的多个窗口) 。
发送和接收消息
// Create a message
public class LoggedInUserChangedMessage : ValueChangedMessage<User>
{
public LoggedInUserChangedMessage(User user) : base(user)
{
}
}
// Register a message in some module
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this, (r, m) =>
{
// Handle the message here, with r being the recipient and m being the
// input message. Using the recipient passed as input makes it so that
// the lambda expression doesn't capture "this", improving performance.
});
// Send a message from some other module
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));
假设此消息类型在简单的消息应用程序中使用,该应用程序显示一个标头,其中包含当前记录的用户的用户名和配置文件图像、一个包含对话列表的面板,以及另一个包含当前对话消息的面板(如果已选中)。 假设这三个部分分别受这些
HeaderViewModelConversationsListViewModel
部分和ConversationViewModel
类型支持。 在此方案中,登录LoggedInUserChangedMessage
操作完成后可能会发送HeaderViewModel
消息,并且这两个其他 viewmodel 都可以为其注册处理程序。 例如,ConversationsListViewModel
将加载新用户的会话列表,如果存在会话,将ConversationViewModel
仅关闭当前对话。
该
IMessenger
实例负责向所有已注册的收件人传送邮件。 请注意,收件人可以订阅特定类型的消息。 请注意,继承的消息类型未在 MVVM Toolkit 提供的默认IMessenger
实现中注册。
不再需要收件人时,应取消注册,以便停止接收邮件。 可以通过邮件类型、注册令牌或收件人取消注册:
// Unregisters the recipient from a message type
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage>(this);
// Unregisters the recipient from a message type in a specified channel
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage, int>(this, 42);
// Unregister the recipient from all messages, across all channels
WeakReferenceMessenger.Default.UnregisterAll(this);
注意:
如前所述,在使用类型时WeakReferenceMessenger
,这不是绝对必要的,因为它使用弱引用来跟踪收件人,这意味着未使用的收件人仍将有资格进行垃圾回收,即使它们仍然具有活动邮件处理程序。 不过,取消订阅它们还是不错的做法,以提高性能。 另一方面,StrongReferenceMessenger
实现使用强引用来跟踪已注册的收件人。 这是出于性能原因完成的,这意味着应手动取消注册每个已注册收件人以避免内存泄漏。 也就是说,只要注册收件人,正在使用的StrongReferenceMessenger
实例就会保留对其的活动引用,这将阻止垃圾回收器能够收集该实例。 可以手动处理此内容,也可以从中ObservableRecipient
继承,默认情况下,在停用收件人的所有邮件注册时会自动将其删除, (查看ObservableRecipient
有关此) 的详细信息的文档。
也可以使用
IRecipient<TMessage>
接口注册消息处理程序。 在这种情况下,每个收件人都需要为给定邮件类型实现接口,并提供Receive(TMessage)
在接收消息时将调用的方法。
// Create a message
public class MyRecipient : IRecipient<LoggedInUserChangedMessage>
{
public void Receive(LoggedInUserChangedMessage message)
{
// Handle the message here...
}
}
// Register that specific message...
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this);
// ...or alternatively, register all declared handlers
WeakReferenceMessenger.Default.RegisterAll(this);
// Send a message from some other module
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));
使用请求消息
信使实例的另一个有用功能是,它们还可用于将值从模块请求到另一个模块。 为此,包包含一个基
RequestMessage<T>
类,可以使用该基类。
// Create a message
public class LoggedInUserRequestMessage : RequestMessage<User>
{
}
// Register the receiver in a module
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
// Assume that "CurrentUser" is a private member in our viewmodel.
// As before, we're accessing it through the recipient passed as
// input to the handler, to avoid capturing "this" in the delegate.
m.Reply(r.CurrentUser);
});
// Request the value from another module
User user = WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();
该
RequestMessage<T>
类包含一个隐式转换器,使从LoggedInUserRequestMessage
其包含User
的对象进行转换成为可能。 这还会检查是否已收到消息的响应,如果不是这种情况,则引发异常。 还可以发送请求消息而不提供此强制响应保证:只需将返回的消息存储在本地变量中,然后手动检查响应值是否可用。 如果方法返回时Send
未收到响应,则这样做不会触发自动异常。
同一命名空间还包括其他方案的基请求消息:
AsyncRequestMessage<T>CollectionRequestMessage<T>
和AsyncCollectionRequestMessage<T>
。
// Create a message
public class LoggedInUserRequestMessage : AsyncRequestMessage<User>
{
}
// Register the receiver in a module
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
m.Reply(r.GetCurrentUserAsync()); // We're replying with a Task<User>
});
// Request the value from another module (we can directly await on the request)
User user = await WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();
结语
到此 CommunityToolkit.Mvvm 工具包的基本功能及用法已罗列完毕,也算是对官方文档的重读。