导图社区 CommunityToolkit
.NET 社区工具包是帮助程序和 API 的集合,适用于所有 .NET 开发人员,并且与任何特定的 UI 平台无关。 该工具包由 Microsoft 维护和发布,是 .NET Foundation 的一部分。
编辑于2024-07-25 11:35:15CommunityToolkit
MVVM
源生成器
从版本 8.0 开始,MVVM 工具包包含全新的 Roslyn 源生成器,有助于在使用 MVVM 体系结构编写代码时大幅减少样本。 它们可以简化需要设置可观察属性、命令等的方案。 如果你不熟悉源生成器,可以在此处详细了解。 以下是工作原理的简化图: 备注: 源生成器可以独立于 MVVM 工具包中的其他现有功能使用,并且可以根据需要随意将源生成器与以前的 API 结合使用和匹配。 也就是说,可以随意地逐渐开始在新文件中使用源生成器,并最终迁移旧文件以使用它们来减少详细程度,但不必始终在整个项目或应用程序中使用任何一种方法。
ObservableProperty
ObservableProperty 类型是一种允许从带批注字段生成可观察属性的特性。 其用途是显著减少定义可观察属性所需的样本量。 平台 API:ObservableProperty、NotifyPropertyChangedFor、NotifyCanExecuteChangedFor、NotifyDataErrorInfo、NotifyPropertyChangedRecipients、ICommand、IRelayCommand、ObservableValidator、PropertyChangedMessage<T>、IMessenger 备注: 带批注字段需要位于具有必要 INotifyPropertyChanged 基础结构的分部类中才能发挥作用。 如果对类型进行嵌套,则必须也将声明语法树中的所有类型批注为分部。 否则,将导致编译错误,因为生成器将无法使用所请求的可观察属性生成该类型的不同分部声明。
工作原理
ObservableProperty 特性可用于批注分部类型中的字段,如下所示: 它将生成如下所示的可观察属性: 它还会使用经过优化的实现完成此过程,因此获得最终结果的速度会更快。 备注: 将基于字段名称创建生成的属性的名称。 生成器假定字段命名采用 lowerCamel、_lowerCamel 或者 m_lowerCamel,并将之转换为 UpperCamel,以遵循正确的 .NET 命名约定。 生成的属性将始终具有公共访问器,但在声明该字段时可以使用任何可见性(建议使用 private)。
发生更改时运行
生成的代码实际上比这要复杂一些,原因是它还公开了一些方法,你可以通过实现这些方法挂钩到通知逻辑,并在属性即将更新时和刚刚更新后运行其他逻辑(如果需要)。 也就是说,生成的代码实际上类似如下: 可以只实现可用方法中任意数量的方法,也可以不实现任何方法。 如果未实现它们(或者只实现一个),编译器将删除整个调用,以便在不需要此附加功能时避免造成性能下降。 备注: 所生成的方法是没有实现的分部方法 ,这意味着如果选择实现这些方法,则不能显式指定其可访问性。 也就是说,这些方法的实现也应仅声明为 partial 方法,并且它们总是隐式具有专用可访问性。 尝试添加显式可访问性(例如添加 public 或 private)将导致错误,因为 C# 中不支持这么做。
仅需要引用属性已设置的新值的逻辑
如果想要运行一些仅需要引用属性已设置的新值的逻辑,则可使用前两个重载。 例如,下面是如何使用前两个重载的示例:
必须更新所设置的旧值和新值的某些状态
如果你有一些更复杂的逻辑并且还必须更新所设置的旧值和新值的某些状态,则可使用其他两个重载。 下面是如何使用其他两个重载的示例:
通知依赖属性
假设你有一个命令,其执行状态依赖于此属性的值。 也就是说,每当此属性发生更改时,命令的执行状态都应失效并再次计算。 换句话说,应再次引发 ICommand.CanExecuteChanged。 可以通过使用 NotifyCanExecuteChangedFor 特性来实现此目的。 要使以上代码生效,目标命令必须是某一 IRelayCommand 属性
请求属性验证
发送通知消息
如果在继承自 ObservableRecipient 的类型中声明属性,则可以使用 NotifyPropertyChangedRecipients 特性指示生成器还要插入代码,以针对属性更改发送一则说明属性已更改的消息。 这样,注册的接收者便可以动态响应更改。 也就是说,应考虑以下代码: 然后,生成的 Broadcast 调用将使用当前 viewmodel 中正在使用的 IMessenger 实例向所有注册订阅者发送新的 PropertyChangedMessage<T>。
添加自定义属性
在某些情况下,在生成的属性上添加一些自定义属性可能会很有用。 为此,你只需在带注释的字段上使用属性列表中的 [property: ] 目标,MVVM Toolkit 就会自动将这些属性转发到生成的属性。 例如,请考虑如下所示的字段: 这将生成一个 Username 属性,其中包含 [JsonRequired] 和 [JsonPropertyName("name")] 这两个属性。 可根据需要使用任意多个针对属性的属性列表,所有这些属性列表都将转发到生成的属性。
RelayCommand
RelayCommand 类型是一个特性,允许为带批注的方法生成中继命令属性。 其目的是完全消除在 viewmodel 中定义命令包装私有方法所需的模板。 备注: 为了正常工作,带批注的方法需要位于分部类中。 如果对类型进行嵌套,则必须也将声明语法树中的所有类型批注为分部。 否则将导致编译错误,因为生成器无法使用请求的命令生成该类型的其他分部声明。 平台 API:RelayCommand、ICommand、IRelayCommand、IRelayCommand<T>、IAsyncRelayCommand、IAsyncRelayCommand<T>、Task、CancellationToken
工作原理
该 RelayCommand 特性可用于对分部类型中的方法进行批注,如下所示: 备注: 将基于方法名称创建生成的命令的名称。 生成器将使用方法名称并在末尾追加“Command”,并且去除“On”前缀(如果存在)。 此外,对于异步方法,“Async”后缀也会在追加“Command”之前去除。
命令参数
该 [RelayCommand] 特性支持使用参数为方法创建命令。 在这种情况下,它会自动将生成的命令更改为 IRelayCommand<T>,从而接受相同类型的参数: 生成的命令将自动使用该参数的类型作为其类型参数。
异步命令
启用和禁止命令
通常情况下,禁用命令并在稍后使其状态失效,然后让这些命令重新检查是否可以执行是非常有用的。 为了支持此功能,RelayCommand 特性会公开 CanExecute 属性,该属性可用于指示用于评估是否可以执行命令的目标属性或方法: 这样,在按钮首次绑定到 UI(例如,绑定到按钮)时会调用 CanGreetUser,然后每次通过命令调用 IRelayCommand.NotifyCanExecuteChanged 时会再次调用它。 例如,下面显示了命令如何绑定到属性以控制其状态: 在此示例中,生成的 SelectedUser 属性在每次其值更改时都会调用 GreetUserCommand.NotifyCanExecuteChanged() 方法。 UI 具有绑定到 GreetUserCommand 的 Button 控件,这意味着每次引发其 CanExecuteChanged 事件时,它都会再次调用其 CanExecute 方法。 这将导致计算包装的 CanGreetUser 方法,从而基于输入 User 实例(UI 中绑定到 SelectedUser 属性)是否为 null 返回按钮的新状态。 这意味着,每当 SelectedUser 发生更改时,GreetUserCommand 都将根据该属性是否具有值来启用或禁用,这是此方案中所需的行为。 备注: 该命令不会自动知道 CanExecute 方法或属性的返回值何时发生更改。 而是由开发人员调用 IRelayCommand.NotifyCanExecuteChanged 以使命令失效,并请求重新评估链接的 CanExecute 方法,然后更新绑定到命令的控件的可视状态。
处理并发执行
每当命令是异步的,都可以将其配置为决定是否允许并发执行。 使用 RelayCommand 特性时,可以通过 AllowConcurrentExecutions 属性设置。 默认值为 false,这意味着,在执行挂起之前,命令将指示其状态为已禁用。 如果改为设置为 true,则可以将任意数量的并发调用排入队列。 请注意,如果命令接受取消令牌,则请求并发执行时也会取消令牌。 主要区别是,如果允许并发执行,该命令将保持启用状态,它将启动新的请求执行,而无需等待上一个执行实际完成。
处理异步异常
异步中继命令处理异常有两种不同的方法: 等待和重新引发(默认):当命令等待完成调用时,任何异常自然都会在同一同步上下文中引发。 这通常意味着引发的异常只会使应用崩溃,该行为与同步命令的行为一致(其中引发的异常也会使应用崩溃)。 将异常流式传送到任务计划程序:如果命令配置为将异常流式传送到任务计划程序,则引发的异常不会使应用崩溃,而是通过公开的 IAsyncRelayCommand.ExecutionTask 以及浮升到 TaskScheduler.UnobservedTaskException 使应用变得可用。 这可实现更高级的方案(例如让 UI 组件绑定到任务并根据操作结果显示不同的结果),但正确使用更为复杂。 默认行为是让命令等待并重新引发异常。 这可以通过 FlowExceptionsToTaskScheduler 属性进行配置: 在这种情况下,不需要 try/catch,因为异常将不再使应用崩溃。 请注意,这还会导致不会自动重新引发其他不相关的异常,因此应仔细决定如何处理每个单独的方案并适当配置其余代码。
取消异步操作的命令
异步命令的最后一个选项是请求生成取消命令功能。 这是 ICommand 包装异步中继命令,可用于请求取消操作。 此命令将自动发出状态信号,以反映它是否可以在任何给定时间使用。 例如,如果链接命令未执行,它会报告其状态报告为也不可执行。 其用法如下所示: 这将导致也会生成 DoWorkCancelCommand 属性。 然后,可以绑定到其他一些 UI 组件,以便用户轻松取消挂起的异步操作。
添加自定义特性
与可观测属性一样,RelayCommand 生成器还会为生成的属性添加自定义特性支持。 若要利用这一点,只需在带注释的方法上使用特性列表中的 [property: ] 目标,MVVM 工具包会将这些属性转发到生成的命令属性。 例如,请考虑如下所示的方法: 这将生成一个 GreetUserCommand 属性,该属性上为 [JsonIgnore] 特性。 可根据需要使用任意多个针对方法的特性列表,所有这些特性列表都将转发到生成的属性。
INotifyPropertyChanged
INotifyPropertyChanged 类型是一个允许将 MVVM 支持代码插入现有类型的属性。 与其他相关属性(ObservableObject和ObservableRecipient)一起,其目的是在需要这些类型的相同功能,但已经从另一种类型中实现目标类型的情况下,为开发人员提供支持。 由于 C# 不允许多重继承,因此可以转而使用这些属性让 MVVM 工具包生成器将相同的代码直接添加到这些类型中,从而避开此限制。 备注: 为了正常工作,带批注的类型需要位于分部类中。 如果对类型进行嵌套,则必须也将声明语法树中的所有类型批注为分部。 否则将导致编译错误,因为生成器无法使用请求的其他代码生成该类型的其他分部声明。 备注: 这些属性仅适用于目标类型不能仅从等效类型(例如从 ObservableObject)继承的情况。 如果可能,推荐的方法是继承,因为它将通过避免在最终程序集中创建重复的代码来减小二进制文件大小。 平台 API:INotifyPropertyChanged、ObservableObject、ObservableRecipient
如何使用它们
使用这些属性中的任何一个都非常简单:只需将它们添加到分部类,相应类型的所有代码就会自动生成到该类型中。 例如,来看看以下示例: 这将在 MyViewModel 类型中生成一个完整的 INotifyPropertyChanged 实现,并附带可用于降低详细程度的其他帮助程序(如 SetProperty)。 以下是各种属性的简要总结: INotifyPropertyChanged:实现接口,并添加帮助程序方法来设置属性和引发事件。 ObservableObject:添加 ObservableObject 类型中的所有代码。 它在概念上等同于 INotifyPropertyChanged,主要区别在于它还实现了 INotifyPropertyChanging。 ObservableRecipient:添加 ObservableRecipient 类型中的所有代码。 特别是,可以将其添加到从 ObservableValidator 继承的类型,以合并两者。
可观测对象
ObservableObject
ObservableObject 是通过实现 INotifyPropertyChanged 和 INotifyPropertyChanging 接口可观察的对象的基类。 它可用作需要支持属性更改通知的各种对象的起点。 平台 API:ObservableObject、TaskNotifier、TaskNotifier<T>
工作原理
ObservableObject 包含以下主要功能: 它提供了对 INotifyPropertyChanged 和 INotifyPropertyChanging 的基本实现,从而公开 PropertyChanged 和 PropertyChanging 事件。 它提供了一系列 SetProperty 方法,这些方法可用于轻松设置继承自 ObservableObject 的类型中的属性值,并自动引发相应的事件。 它提供了 SetPropertyAndNotifyOnCompletion 方法,该方法与 SetProperty 类似,但能够设置 Task 属性并在分配的任务完成后自动引发通知事件。 它公开了 OnPropertyChanged 和 OnPropertyChanging 方法,这些方法可在派生类型中重写,以自定义引发通知事件的方式。
简单属性
下面是如何实现自定义属性的通知支持的示例: 提供的 SetProperty<T>(ref T, T, string) 方法检查属性的当前值,如果不同,则对其进行更新,然后还会自动引发相关事件。 属性名称是通过使用 [CallerMemberName] 属性自动捕获的,因此无需手动指定要更新的属性。
包装不可观察的模型
例如,使用数据库项时,常见方案是创建一个包装“可绑定”模型,该模型中继数据库模型的属性,并在需要时引发属性更改通知。 如果想要向未实现 INotifyPropertyChanged 接口的模型注入通知支持,也需要这样做。 ObservableObject 提供了一种专用方法,使此过程更简单。 在以下示例中,User 是一个直接映射数据库表的模型,不继承自 ObservableObject: 在本例中,我们将使用 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: 在这里,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 实例。
ObservableRecipient
ObservableRecipient 类型是可观察对象的基类,这些对象还充当消息收件人。 此类是 ObservableObject 的扩展,它还对使用 IMessenger 类型提供内置支持。 平台 API:ObservableRecipient、ObservableObject、IMessenger、WeakReferenceMessenger、IRecipient<TMessage>、PropertyChangedMessage<T>
工作原理
ObservableRecipient 类型旨在用作视图模型的基础,这些模型还使用 IMessenger 功能,因为它对其提供内置支持。 具体而言: 它有一个无参数构造函数和一个采用 IMessenger 实例的构造函数,用于依赖关系注入。 它还公开一个 Messenger 属性,该属性可用于在视图模型中发送和接收消息。 如果使用无参数构造函数,WeakReferenceMessenger.Default 实例将分配给 Messenger 属性。 它公开 IsActive 属性来激活/停用视图模型。 在此上下文中,“激活”意味着给定视图模型被标记为正在使用中,这样它将开始侦听已注册的消息、执行其他设置操作等。属性更改值时,调用了两个相关方法 - OnActivated 和 OnDeactivated。 默认情况下,OnDeactivated 会自动从所有已注册的消息中注销当前实例。 为了获得最佳结果并避免内存泄漏,建议使用 OnActivated 注册到消息,并使用 OnDeactivated 执行清理操作。 此模式支持多次启用/禁用视图模型,同时在每次停用时可以安全地收集,没有内存泄漏的风险。 默认情况下,OnActivated 会自动注册通过 IRecipient<TMessage> 接口定义的所有消息处理程序。 它公开一个 Broadcast<T>(T, T, string) 方法,该方法通过 Messenger 属性提供的 IMessenger 实例发送 PropertyChangedMessage<T> 消息。 这可用于轻松广播视图模型属性中的更改,而无需手动检索要使用的 Messenger 实例。 此方法由各种 SetProperty 方法的重载使用,这些方法具有额外的 bool broadcast 属性来指示是否也发送消息。 下面是在激活后接收 LoggedInUserRequestMessage 消息的视图模型示例: 在上面的示例中,OnActivated 使用该方法作为要调用的操作,自动将实例注册为 LoggedInUserRequestMessage 消息的接收方。 并非必须使用 IRecipient<TMessage> 接口,也可手动完成注册(即使只使用内联 Lambda 表达式):
ObservableValidator
ObservableValidator 是实现 INotifyDataErrorInfo 接口的基类,它支持验证向其他应用程序模块公开的属性。 它也继承自 ObservableObject,因此它还可实现 INotifyPropertyChanged 和 INotifyPropertyChanging。 它可用作需要支持属性更改通知和属性验证的各种对象的起点。 平台 API:ObservableValidator、ObservableObject
工作原理
ObservableValidator 包含以下主要功能: 它提供对 INotifyDataErrorInfo 的基本实现,从而公开 ErrorsChanged 事件和其他必要的 API。 它提供一系列额外的 SetProperty 重载(ObservableObject 基类提供的重载除外),这些重载提供在更新属性值之前自动验证属性和引发必要事件的功能。 它公开了许多 TrySetProperty 重载,这些重载类似于 SetProperty 但仅在验证成功时更新目标属性,并在出错时返回生成的错误以供进一步检查。 它公开了 ValidateProperty 方法,这对于手动触发对特定属性的验证非常有用,以防其值尚未更新,但其验证依赖于已更新的另一个属性的值。 它公开了 ValidateAllProperties 方法,这会自动执行对当前实例中所有公共实例属性的验证,前提是它们至少应用了一个 [ValidationAttribute]。 它公开了 ClearAllErrors 方法,该方法在重置绑定到用户可能需要再次填充的某个表单的模型时非常有用。 它提供许多构造函数,这些函数允许传递不同的参数来初始化将用于验证属性的 ValidationContext 实例。 使用可能需要其他服务或选项才能正常工作的自定义验证特性时,这尤其有用。
简单属性
下面是如何实现支持更改通知和验证功能的属性的示例: 在这里,我们调用由 ObservableValidator 公开的 SetProperty<T>(ref T, T, bool, string) 方法,并将该附加 bool 参数设置为 true,以指示我们还希望在属性的值更新时对其进行验证。 ObservableValidator 将使用应用于该属性的特性指定的所有检查自动对每个新值运行验证。 然后,其他组件(如 UI 控件)可以与 viewmodel 交互并修改其状态,以反映 viewmodel 中当前存在的错误,方法是注册到 ErrorsChanged 并使用 GetErrors(string) 方法检索已修改的每个属性的错误列表。
自定义验证方法
有时,进行属性验证需要 viewmodel 具有对其他服务、数据或其他 API 的访问权限。 有多种方法可以向属性添加自定义验证,具体取决于应用场景和所需的灵活性级别。 以下示例说明如何使用 [CustomValidationAttribute] 类型来指示需要调用特定方法来执行对属性的其他验证: 在本例中,我们有一个静态的 ValidateName 方法,它将通过注入到 viewmodel 中的服务对 Name 属性执行验证。 此方法接收正在使用的 name 属性值和 ValidationContext 实例,其中包含诸如 viewmodel 实例、正在验证的属性的名称以及(可选)服务提供程序和一些我们可以使用或设置的自定义标志等内容。 在本例中,我们从验证上下文中检索 RegistrationForm 实例,然后在其中使用注入的服务来验证属性。 请注意,此验证将与其他特性中指定的验证同时执行,因此我们可以任意方式自由组合自定义验证方法和现有的验证属性。
自定义验证属性
执行自定义验证的另一种方法是实现自定义 [ValidationAttribute],然后将验证逻辑插入到重写的 IsValid 方法中。 与上述方法相比,这可实现额外的灵活性,因为它可以轻松地在多个位置重复使用同一特性。 假设我们想要根据属性相对于同一 viewmodel 中另一个属性的相对值来验证该属性。 第一步是定义自定义 [GreaterThanAttribute],如下所示: 接下来,我们可以将此特性添加到 viewmodel 中: 在本例中,我们有两个数值属性,这些属性必须位于特定范围中,并且彼此之间存在特定关系(A 需要大于 B)。 我们在第一个属性上添加了新的 [GreaterThanAttribute],还在 B 的 setter 中添加了对 ValidateProperty 的调用,以便每次 B 更改时再次验证 A(因为它的验证状态取决于它本身)。 我们只需在 viewmodel 中使用这两行代码来启用此自定义验证,同时还可享受可重用的自定义验证特性带来的好处,该特性在应用程序中的其他 viewmodel 中也很有用。 此方法还有助于实现代码模块化,因为验证逻辑现在已与 viewmodel 定义本身完全分离。
命令
Relaycommand 和 Relaycommand<T>
RelayCommand 和 RelayCommand<T> 是 ICommand 实现,这些实现可向视图公开方法或委托。 这些类型充当在 viewmodel 和 UI 元素之间绑定命令的方法。 平台API:RelayCommand、RelayCommand<T>、IRelayCommand、IRelayCommand<T>
工作原理
RelayCommand 和 RelayCommand<T> 具有以下主要功能: 它们提供 ICommand 接口的基本实现。 它们还实现 IRelayCommand(和 IRelayCommand<T>)接口,该接口公开 NotifyCanExecuteChanged 方法以引发 CanExecuteChanged 事件。 它们公开采用委托(如 Action 和 Func<T>)的构造函数,从而允许包装标准方法和 Lambda 表达式。
使用 ICommand
下面演示如何设置简单的命令: 然后,相对 UI 可以是(使用 WinUI XAML): Button 绑定到 viewmodel 中的 ICommand,它包装专用 IncrementCounter 方法。 TextBlock 显示 Counter 属性的值,并在每次属性值更改时更新。
AsyncRelayCommand 和 AsyncRelayCommand<T>
AsyncRelayCommand 和 AsyncRelayCommand<T> 是 ICommand 实现,它们扩展了 RelayCommand 提供的功能,并支持异步操作。 平台 API:AsyncRelayCommand、AsyncRelayCommand<T>、RelayCommand、IAsyncRelayCommand、IAsyncRelayCommand<T>
工作原理
AsyncRelayCommand 和 AsyncRelayCommand<T> 具有以下主要功能: 它们扩展了库中包含的同步命令的功能,并支持 Task 返回委托。 它们可以使用附加 CancellationToken 参数包装异步函数以支持取消,并公开 CanBeCanceled 和 IsCancellationRequested 属性以及 Cancel 方法。 它们公开可用于监视挂起操作进度的 ExecutionTask 属性,以及可用于检查操作完成时间的 IsRunning 属性。 这对于将命令绑定到 UI 元素(如加载指示器)特别有用。 它们实现 IAsyncRelayCommand 和 IAsyncRelayCommand<T> 接口,这意味着 viewmodel 可以轻松地使用这些接口来公开命令,从而减少类型之间的紧密耦合。 例如,这样就可以轻松地将命令替换为公开相同公共 API 图面的自定义实现(如果需要)。
使用异步命令
假设一个方案类似于 RelayCommand 示例中所述的方案,但命令执行异步操作: 使用相关的 UI 代码: 单击 Button 时,将调用该命令并更新 ExecutionTask。 操作完成后,该属性将引发一个通知,该通知反映在 UI 中。 在这种情况下,将同时显示任务状态和当前任务结果。 请注意,若要显示任务的结果,则必须使用 TaskExtensions.GetResultOrDefault 方法 - 这样就可以无需阻止线程(可能导致死锁)即可访问尚未完成的任务的结果。
Messenger
IMessenger 接口是可用于在不同对象之间交换消息的类型协定。 这可用于分离应用程序的不同模块,而无需保留对所引用类型的强引用。 还可以将消息发送到特定通道,由令牌唯一标识,并在应用程序的不同部分中具有不同的信使。 MVVM 工具包提供两种现用的实现: WeakReferenceMessenger 和 StrongReferenceMessenger :前者在内部使用弱引用,为收件人提供自动内存管理,而后者使用强引用,并要求开发人员在不再需要收件人时手动取消订阅收件人(有关如何注销消息处理程序的更多详细信息,可在下面找到),但这一点换来的是提供更好的性能,而且内存使用量要少得多。 平台 API:IMessenger,WeakReferenceMessenger,StrongReferenceMessenger,IRecipient<TMessage>,MessageHandler<TRecipient, TMessage>,ObservableRecipient,RequestMessage<T>,AsyncRequestMessage<T>,CollectionRequestMessage<T>,AsyncCollectionRequestMessage<T>。
工作原理
实现 IMessenger 的类型负责维护收件人(消息接收方)与其已注册的邮件类型(包含相对消息处理程序)之间的链接。 可以使用消息处理程序将任何对象注册为给定邮件类型的收件人,每当使用 IMessenger 实例发送该类型的消息时,都会调用该对象。 还可以通过特定的通信通道道(每个通道都由唯一令牌标识)发送消息,以便多个模块可以交换同一类型的消息,而不会造成冲突。 在没有令牌的情况下发送的消息使用默认共享通道。 有两种方法可以执行消息注册:通过 IRecipient<TMessage> 接口或使用充当消息处理程序的 MessageHandler<TRecipient, TMessage> 委托。 第一个允许你向 RegisterAll 扩展的单个调用注册所有处理程序,该扩展会自动注册所有声明的消息处理程序的收件人,而后者在需要更多灵活性或想要将简单的 lambda 表达式用作消息处理程序时非常有用。 WeakReferenceMessenger 和 StrongReferenceMessenger 还公开一个 Default 属性,该属性提供内置于包中的线程安全实现。 如果需要,还可以创建多个信使实例,例如,如果使用 DI 服务提供程序将另一个信使实例注入到应用的不同模块(例如,在同一进程中运行的多个窗口)。 备注: 由于 WeakReferenceMessenger 类型更易于使用,并且与 MvvmLight 库中的信使类型的行为匹配,因此它是 MVVM 工具包中 ObservableRecipient 类型使用的默认类型。 通过将实例传递给该类的构造函数,仍可使用 StrongReferenceType。
发送和接收消息
考虑以下情况: 假设此消息类型用于简单的消息应用程序,它显示具有当前记录用户的用户名和配置文件图像的标头、包含对话列表的面板,以及包含当前对话中消息的另一个面板(如果已选中)。 假设这三个部分分别受 HeaderViewModel、ConversationsListViewModel 和 ConversationViewModel 类型支持。 在此方案中,LoggedInUserChangedMessage 消息可能在登录操作完成后由 HeaderViewModel 发送,并且这两个其他视图模型都可以为其注册处理程序。 例如,ConversationsListViewModel 将加载新用户的聊天列表,如果存在对话,ConversationViewModel 将仅关闭当前对话。 IMessenger 实例负责将消息传送到所有已注册收件人。 请注意,收件人可以订阅特定类型的邮件。 请注意,继承的消息类型未在 MVVM 工具包提供的默认 IMessenger 实现中注册。 当不再需要收件人时,应将其注销,以便停止接收消息。 可以按消息类型、注册令牌或收件人取消注册: 警告: 如前所述,使用 WeakReferenceMessenger 类型时,这并非严格必要,因为它使用弱引用来跟踪收件人,这意味着未使用的收件人仍有资格进行垃圾回收,即使它们仍然具有活动消息处理程序。 不过,取消订阅它们仍然是一个很好的提高性能的做法。 另一方面,StrongReferenceMessenger 实现使用强引用来跟踪已注册的收件人。 这是出于性能原因完成的,这意味着应手动取消注册每个已注册的收件人,以避免内存泄漏。 也就是说,只要注册收件人,正在使用的 StrongReferenceMessenger 实例就会保留对其的活动引用,这将阻止垃圾回收器能够收集该实例。 可以手动处理此内容,也可以从 ObservableRecipient 继承,默认情况下,其会自动处理停用收件人的所有消息注册(有关此内容的详细信息,请参阅有关 ObservableRecipient 的文档)。 也可以使用 IRecipient<TMessage> 接口注册消息处理程序。 在这种情况下,每个收件人都需要为给定消息类型实现接口,并提供一个在接收邮件时将调用的 Receive(TMessage) 方法,如下所示:
使用请求消息
依赖关系注入
Ioc 控制反转
使用 MVVM 模式提高应用程序代码库中的模块化程度的最常用模式是使用某种形式的反转控制。 其中有一种最常见的解决方案使用依赖关系注入,该解决方案存在于创建多个注入后端类的服务(即以参数的形式传递给 viewmodel 构造函数)的过程中,这允许使用这些服务的代码不依赖这些服务的实现详细信息,并且也可以轻松地交换这些服务的具体实现。 这种模式还可以通过服务将特定于平台的功能抽象出来,然后在需要的地方注入这些功能,从而使后端代码可以轻松使用这些功能。 MVVM 工具包并没有提供内置的 API 来促进这种模式的使用,因为已存在专门用于此的专用库(如 Microsoft.Extensions.DependencyInjection 包),它提供了功能齐全、强大的 DI API 集,并充当了易于设置和使用的 IServiceProvider。 以下指南将参考此库,并提供有关如何使用 MVVM 模式将其集成到应用程序中的一系列示例。 平台 API:Ioc
配置和解析服务
第一步是声明 IServiceProvider 实例,并在启动时初始化所有必要的服务。 例如,在 UWP 上(但类似的设置也可以在其他框架上使用): 其中,Services 属性在启动时初始化,所有应用程序服务和 viewmodel 都已注册。 还有一个新的 Current 属性可用于从应用程序的其他视图中轻松访问 Services 属性。 例如: 这里最关键的一点是,每个服务很可能都在使用特定于平台的 API,但由于这些 API 都通过我们的代码所使用的接口进行了抽象,因此只要我们只是解析实例并使用它来执行操作,就不需要担心这些 API。
构造函数注入
“构造函数注入”是可用的一项强大功能,这意味着 DI 服务提供商能够在创建所请求类型的实例时自动解析已注册服务之间的间接依赖关系。 请考虑下面的服务: 在这里,我们有一个 FileLogger 类型,该类型实现 IFileLogger 接口,并且需要 IFilesService 和 IConsoleService 实例。 构造函数注入意味着 DI 服务提供商会自动收集所有必要的服务,如下所示: DI 服务提供商将自动检查是否注册了所有必要的服务,然后它将检索它们并调用已注册的 IFileLogger 具体类型的构造函数,以获取要返回的实例。
Viewodel 怎么样
服务提供商的名称中包含“service”,但它实际上可用于解析任何类的实例,包括 viewmodel! 上述相同的概念仍然适用,包括构造函数注入。 假设我们有一个 ContactsViewModel 类型,通过它的构造函数使用 IContactsService 和 IPhoneService 实例。 我们可以采用 ConfigureServices 方法,如下所示: 然后在我们的 ContactsView 中,我们将按如下所示分配数据上下文:
更多文档
有关 Microsoft.Extensions.DependencyInjection 的详细信息,请参阅此处。
Diagnostics
用于有效验证方法参数并在错误代码路径中引发异常的 API。 它旨在用于帮助简化所有参数检查,并使其更具表达性和易读性,同时提高 codegen 质量和性能。 此包可以通过 NuGet 安装,并且包含以下多目标: .NET Standard 2.0 .NET Standard 2.1 .NET 6
Guard
Guard 可用于以简化的方式验证方法参数,与手动写入检查和引发异常相比,该方法速度更快、更简洁、更具表现力且不易出错。 平台 API:Guard,CallerArgumentExpressionAttribute
工作原理
生成这些 Guard API 时要考虑的三个核心原则:
速度快
为了达到此目的,所有 API 都设计为在调用方尽可能少地生成代码,并且每个 Guard API(几乎总是)会内联。 此外,还使用 T4 模板生成专用方法,以在处理常见类型(例如基元数值类型)时获得最有效的程序集代码。
有帮助
每个 Guard API 都会在适用时生成一条详细的异常消息,明确指出出现的问题以及其他信息(例如当前变量值)。
直观
为了实现这一点,所有 Guard API 都有富有表现力且显而易见的名称,使每个 API 应该执行何种操作一目了然。 这避免了开发人员编写检查时的负担,使其可以使用自然语言表达条件。
语法
使用新增的 Guard.APIs 来验证输入参数: Guard API 将以尽可能快的方式执行所需的检查,并在失败时引发的相应异常,同时显示格式良好的消息。 备注: Guard API 依赖于 [CallerArgumentExpression] 自动推断要验证的参数的名称。 这要求在正在使用的项目中启用 C# 10。 如果使用的是较低版本的语言,则需要手动传递参数名称,例如,使用 nameof() 来引用它(例如 Guard.IsNotNull(array, nameof(array))))。
方法
常规
比较
字符串
集合
任务
示例:参见单元测试
GitHub
ThrowHelper
ThrowHelper 类是一种帮助程序类型,可用于高效引发异常。 它旨在支持 Guard API,主要用于开发人员需要对引发的异常类型或要包含的确切异常消息进行精细控制的情况。 平台 API:ThrowHelper、Guard
语法
HighPerformance
Windows
MAUI