点击上方蓝字
关注我们
(本文阅读时间:10分钟)
我们很高兴地宣布正式推出新的 .NET 社区工具包,现在已经在NuGet上发布了8.0.0版本!这是一个重要版本,包括大量新功能、改进、优化、错误修复,许多反映了全新项目结构和组织的重构,这篇文章将详细描述这些内容。
与每个社区工具包版本一样,所有的更改都受到使用该工具包的微软团队和社区其他开发人员反馈的影响。我们非常感谢所有做出贡献并不断帮助 .NET 社区工具包变得更好的人!
.NET 社区工具包中有什么?
.NET 社区工具包是一组适用于所有 .NET 开发人员的帮助程序和 API,独立于任何特定的 UI 平台。该工具包由 Microsoft 维护和发布,是 .NET 基金会的一部分。它也被一些内部项目和收件箱应用程序使用,例如 Microsoft Store。从新的 8.0.0 版本开始,该项目现在位于 GitHub 上的 CommunityToolkit/dotnet 存储库中,其中包括作为 Toolkit 一部分的所有库。
所有可用的 API 都不依赖于任何特定的运行时或框架,因此所有 .NET 开发人员都可以使用它们。这些库是从 .NET Standard 2.0 到 .NET 6 的多目标库,所以它们既可以支持尽可能多的平台,又可以在与较新的运行时一起使用时进行优化以获得最佳性能。
.NET 社区工具包中的库包括:
CommunityToolkit.Common
CommunityToolkit.Mvvm(又名“微软 MVVM 工具包”)
CommunityToolkit.Diagnostics
CommunityToolkit.HighPerformance
CommunityToolkit/dotnet:
https://github.com/CommunityToolkit/dotnet
社区工具包历史一览
您可能想知道为什么 .NET 社区工具包的第一个版本是 8.0.0 版本。好问题!原因是 .NET 社区工具包的所有库最初都是Windows 社区工具包的一部分,它是帮助程序、扩展和自定义控件的集合,和自定义控件,可简化和演示为 Windows 10 和 Windows 11 构建 UWP 和 .NET 应用程序的常见开发人员任务。
随着时间的推移,仅针对 .NET 且没有任何 Windows 特定依赖项的 API 数量不断增加,我们决定将它们拆分到一个单独的项目中,以便它们可以独立发展,并且对于不进行任何 Windows 开发的 .NET 开发人员来说也更容易找到。.NET 社区工具包就是这样诞生的。这也使我们更容易且更好地组织文档,现在每个特定于平台的工具包都有其单独的文档。
由于分支之前的 Windows 社区工具包的最后一个版本是 7.1.x,我们决定遵循该语义版本号以使现有用户更容易理解转换,这就是 .NET 社区工具包的第一个版本是 8.0.0 的原因。 展望未来,它将与 Windows 社区工具包分开进行版本控制,因为每个项目都有自己独立的路线图和发布时间表。
搞清楚这些之后,现在让我们深入了解 .NET 社区工具包库的这个新的主要版本中的所有新功能!
Windows 社区工具包
https://github.com/CommunityToolkit/WindowsCommunityToolkit
文档
https://docs.microsoft.com/zh-cn/dotnet/communitytoolkit/?ocid=AID3052907
MVVM 工具包
正如之前在7.0 版本中宣布的那样,.NET 社区工具包的主要组件之一是 MVVM 工具包:一个现代的、快速的、平台无关和模块化的 MVVM 库。这与 Microsoft Store、照片应用程序等使用的 MVVM 库相同!
MVVM 工具包受到MvvmLight的启发,并且由于该库已被弃用,MVVM工具包也就是MvvmLight的官方替代品。我们在开发 MVVM 工具包的同时也与Laurent Bugnion合作,他支持 MVVM 工具包作为现有 MvvmLight 用户的升级道路(我们也有这方面的迁移文档)。
MVVM工具包是基于以下几个关键原则构建的:
平台无关:意味着它不依赖于特定的 UI 框架。您可以使用它在 UWP、WinUI 3、MAUI、WPF、Avalonia、Uno 等之间共享代码!
运行时无关:该库支持多目标并支持低至 .NET Standard 2.0的环境,这意味着您可以在现代运行时(例如 .NET 6)上运行时获得性能改进,并且即使在 .NET 框架上仍然可以使用它。
易于上手和使用:对使用的应用程序结构或编码模式没有严格的要求。您可以使用该库来适应您自己的架构和风格。
À la carte:所有组件都是独立的,也可以单独使用。没有强迫您使用“全部”的方法:如果您只想使用整个库中的一种类型,您可以做得很好,然后根据需要逐渐开始使用更多功能。
参考实现:所有可用的 API 都是精简和高性能的,为 .NET 基类库中包含的接口提供“参考实现”,但缺乏直接使用它们的具体类型。例如,您将能够找到 INotifyPropertyChanged 或 ICommand 等接口的“参考实现”。
7.0 版本:
https://blogs.windows.com/windowsdeveloper/2021/03/16/announcing-windows-community-toolkit-v7-0/
MvvmLight:
https://www.nuget.org/packages/MvvmLight
Laurent Bugnion:
https://twitter.com/LBugnion
迁移文档:
https://docs.microsoft.com/zh-cn/dotnet/communitytoolkit/mvvm/migratingfrommvvmlight?ocid=AID3052907
MVVM 工具包源生成器
MVVM Toolkit 8.0.0 版本中最大的新特性是新的 MVVM 源代码生成器,它旨在大大减少使用 MVVM 设置应用程序所需的样板代码。与我们在 7.1.0 中发布的预览生成器相比,它们也被完全重写为增量生成器,这意味着它们的运行速度将比以前更快,并且即使在处理大型项目时也会有助于保持 IDE 的快速响应。
您可以在此处找到有关新源生成器的所有文档,如果您更喜欢视频版本,James Montemagno 还制作了几个关于它们的视频。让我们也回顾一下由源生成器提供支持的主要功能,您可以在 MVVM 工具包中找到这些功能。
MVVM 源代码生成器:
https://docs.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/overview?ocid=AID3052907
在 7.1.0 中发布的预览生成器:
https://devblogs.microsoft.com/ifdef-windows/windows-community-toolkit-7-1-preview-release/?ocid=AID3052907
增量生成器:
https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md
视频:
https://www.youtube.com/watch?v=AXpTeiWtbC8&t=600s
命令
创建命令可能是非常重复的,在为每个方法设置一个属性的需求下,我们希望以抽象的方式向应用程序中用于调用它们的各种UI组件(如按钮)公开这些方法。
这就是新的 [RelayCommand] 属性发挥作用的地方:这将使 MVVM 工具包自动生成具有正确签名的命令(使用库中包含的 RelayCommand 类型),具体取决于带注释的方法。
作为比较,以下是从前人们通常会如何设置命令的代码:
private IRelayCommand<User> greetUserCommand;public IRelayCommand<User> GreetUserCommand => greetUserCommand ??= new RelayCommand<User>(GreetUser);private void GreetUser(User user)
{Console.WriteLine($"Hello {user.Name}!");
}
现在可以简化为:
[RelayCommand]
private void GreetUser(User user)
{Console.WriteLine($"Hello {user.Name}!");
}
源生成器将负责根据带注释的方法创建正确的 GreetUserCommand 属性。此外,您可以指定 CanExecute 方法,还可以控制异步命令的并发级别。还有其他选项可以微调生成的命令的行为,您可以在我们的文档中了解更多信息。
在我们的文档中
https://docs.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/?ocid=AID3052907
可观察的属性
编写可观察属性可能非常冗长,尤其是当您还必须添加额外的逻辑来处理被通知的依赖属性时。现在,通过使用 MVVM 工具包中的新属性,并让源生成器在幕后创建可观察的属性,所有这些都可以大大简化。
这些新属性是 [ObservableProperty]、[NotifyPropertyChangedFor] 和 [NotifyCanExecuteChangedFor]、[NotifyDataErrorInfo] 和 [NotifyPropertyChangedRecipients]。接下来让我们快速回顾一下所有这些新属性可以做什么。
考虑一个场景,其中有两个可观察属性,一个依赖属性和上面定义的命令,当两个可观察属性中的任何一个发生变化时,都需要通知依赖属性和命令。也就是说,每当 FirstName 或 LastName 更改时,也会通知 FullName 以及 GreetUserCommand。
这就是过去的做法:
private string? firstName;public string? FirstName
{get => firstName;set{if (SetProperty(ref firstName, value)){OnPropertyChanged(nameof(FullName));GreetUserCommand.NotifyCanExecuteChanged();}}
}private string? lastName;public string? LastName
{get => lastName;set{if (SetProperty(ref lastName, value)){OnPropertyChanged(nameof(FullName));GreetUserCommand.NotifyCanExecuteChanged();}}
}public string? FullName => $"{FirstName} {LastName}";
现在可以全部改写如下:
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(GreetUserCommand))]
private string? firstName;[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(GreetUserCommand))]
private string? lastName;public string? FullName => $"{FirstName} {LastName}";
MVVM 工具包将处理这些属性的代码生成,包括插入所有逻辑以引发指定的属性更改或执行更改事件。
但是等等,还有更多的特性!当使用 [ObservableProperty] 生成可观察属性时,MVVM 工具包 现在还将生成两个没有实现的部分方法:On<PROPERTY_NAME>Changing 和 On<PROPERTY_NAME>Changed 。这些方法可用于在更改属性时注入额外的逻辑,而无需回退到使用手动属性。请注意,由于这两个方法是部分的、返回 void 且没有定义,如果未实现它们,C# 编译器将完全删除它们,这意味着它们在不使用时会消失并且不会添加到应用程序中间。
这是如何使用它们的示例:
[ObservableProperty]
private string name;partial void OnNameChanging(string name)
{Console.WriteLine($"The name is about to change to {name}!");
}partial void OnNameChanged(string name)
{Console.WriteLine($"The name just changed to {name}!");
}
当然,您也可以只使用这两种方法中的一种,或者不使用任何一种。
从上面的代码片段中,源生成器将生成类似于以下的代码:
public string Name
{get => name;set{if (!EqualityComparer<string>.Default.Equals(name, value)){OnNameChanging(value);OnPropertyChanging();name = value;OnNameChanged();OnPropertyChanged();}}
}partial void OnNameChanging(string name);partial void OnNameChanged(string name);
[ObservableProperty] 属性还支持验证:如果表示属性的任何字段具有一个或多个继承自 ValidationAttribute 的属性,这些属性将自动复制到生成的属性中,因此在使用 ObservableValidator 创建可验证的属性时也完全支持这种方法。如果您还希望在设置其值时验证该属性,您还可以添加 [NotifyDataErrorInfo] 以在属性设置器中生成验证代码。
[ObservableProperty]还有更多可用的功能,就像命令一样,您可以关于它们的信息并在我们的文档中查看更多示例。
在我们的文档中
https://docs.microsoft.com/zh-cn/dotnet/communitytoolkit/mvvm/generators/overview?ocid=AID3052907
取消对命令的支持
[RelayCommand] 属性中添加了一个新属性,可用于指示源生成器在原始命令旁边生成取消命令。此取消命令可用于取消异步命令的执行。
这也展示了 [RelayCommand] 如何自动适应异步方法和接受参数的方法,并在后台创建异步命令的实现。这还启用了其他功能,例如易于设置绑定以显示进度指示器等等。
这是如何使用它们的示例:
[RelayCommand(IncludeCancelCommand = true)]
private async Task DoWorkAsync(CancellationToken token)
{// 使用取消支持做一些长期运行的工作
}
从这个小片段中,生成器将生成以下代码:
private AsyncRelayCommand? doWorkCommand;public IAsyncRelayCommand DoWorkCommand => doWorkCommand ??= new AsyncRelayCommand(DoWorkAsync);ICommand? doWorkCancelCommand;public ICommand DoWorkCancelCommand => doWorkCancelCommand ??= IAsyncRelayCommandExtensions.CreateCancelCommand(UpdateSomethingCommand);
生成的代码与 IAsyncRelayCommandExtensions.CreateCancelCommand API 中的逻辑相结合,让您只需一行代码即可生成命令,在工作开始或运行时通知 UI,并具有自动并发控制(命令是 当它已经运行时命令是默认禁用的)。每当主命令开始或结束运行时,将通知单独的取消命令,并在执行时向传递给主命令包装的方法的令牌发出取消信号。所有这些,完全抽象出来,只需一个属性即可轻松访问。
对生成属性的 Broadcast 的更改支持
我们还添加了一个新的 [NotifyPropertyChangedRecipients] 属性,该属性可用于从继承自 ObservableRecipient(或使用 [ObservableRecipient] 注释的类型)生成的可观察属性。使用它将生成对 Broadcast 方法的调用,以向所有其他订阅组件发送有关刚刚发生的属性更改的消息。这在视图模型的属性更改还需要通知应用程序中的其他组件的情况下很有用(假设有一个 IsLoggedIn 布尔属性,当用户登录时更新;这可以通知并触发应用程序来刷新 Broadcast 消息)。
它可以按如下方式使用:
[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string name;
这将产生与此类似的代码:
public string Name
{get => name;set{if (!EqualityComparer<string>.Default.Equals(name, value)){OnNameChanging(value);OnPropertyChanging();string oldValue = name;name = value;Broadcast(oldValue, value, nameof(Name));OnNameChanged();OnPropertyChanged();}}
}
这是另一个增强生成的属性并确保它们可以在几乎所有场景中使用而不会被迫回退到手动属性的功能。
ViewModel 组成
C# 没有多重继承,这有时会成为障碍。
如果有一个必须从特定类型继承的视图模型,但您还想向其中注入 INotifyPropertyChanged 支持,或者让它也从 ObservableRecipient 继承以访问其 API,该怎么办?
MVVM 工具包现在通过引入代码生成属性来解决这个问题,这些属性允许将这些类型的逻辑注入到任意类中。它们是 [INotifyPropertyChanged]、[ObservableObject] 和 [ObservableRecipient]。
将它们添加到一个类将导致 MVVM 工具包源代码生成器将该类型的所有逻辑包含到该类中,就好像该类也继承自该类型一样。例如:
[INotifyPropertyChanged]
partial class MyObservableViewModel : DatabaseItem
{
}
此 MyObservableViewModel 将像您所期望的那样继承自 DatabaseItem,但使用 [INotifyPropertyChanged] 将使其也支持 INotifyPropertyChanged,以及 ObservableObject 自身包含的所有帮助 API。
我们仍然建议在需要时从基本类型(例如 ObservableObject)继承,因为这也有助于减少二进制大小,但是在需要的时候以这种方式注入代码的能力可以帮助在无法改变视图模型的基本类型的情况下解决c#的限制,就像上面的例子一样。
改进的 Messenger API
MVVM 工具包 中另一个常用的特性是 IMessenger 接口,它是一种类型合约,可用于在不同对象之间交换消息。
这对于解耦应用程序的不同模块而不必保持对引用类型的强引用很有用。还可以将消息发送到特定通道,由令牌唯一标识,并在应用程序的不同部分具有不同的信使。
MVVM 工具包 提供了这个接口的两种实现:
WeakReferenceMessenger:它不会固定收件人并允许收集他们。这是通过依赖句柄实现的,这是一种特殊类型的 GC 引用,它允许此信使确保始终允许收集已注册的接收者,即使已注册的处理程序将它们引用回来,但不存在对它们的其他未完成的强引用。
StrongReferenceMessenger:这是一个信使实现,它对已注册的接收者进行根权限化,以确保它们保持活跃状态,即使信使是唯一引用它们的对象。
下面是一个如何使用这个接口的小例子:
// 声明消息
public sealed record LoggedInUserChangedMessage(User user);
// 明确注册收件人...
messenger.Register<MyViewModel, LoggedInUserChangedMessage>(this, static (r, m) =>
{
// 在这里处理消息,r 是接收者,m 是接收者
// 输入消息。使用作为输入传递的接收者使得
// ambda 表达式不捕获“this”,从而提高性能。
});// ... 或者让视图模型实现 IRecipient<TMessage>......
class MyViewModel : IRecipient<LoggedInUserChangedMessage>
{public void Receive(LoggedInUserChangedMessage message){// 在这里处理消息}
}// ... 然后通过接口注册(其他API也可用)messenger.Register<LoggedInuserChangedMessage>(this);// 从其他模块发送消息
messenger.Send(new LoggedInUserChangedMessage(user));
由于新提供的公共 DependentHandle API,这个新版本的 MVVM 工具包中的信使实现在 .NET 6 中得到了高度优化,它允许信使类型变得比以前更快,并提供完全零分配的消息广播。以下是一些基准,展示了 MVVM 工具包中的信使与其他广泛使用的 MVVM 库中的其他几种等效类型的比较:
方法 | 中位数 | 错误 | 标准差 | 比率 | 比率标准差 | 第0代 | 第一代 | 已分配 |
MVVMToolkitStrong | 4.025 ms | 0.0177 ms | 0.0147 ms | 1 | 0 | – | – | – |
MVVMToolkitWeak | 7.549 ms | 0.0815 ms | 0.0762 ms | 1.87 | 0.02 | – | – | – |
MvvmCrossStrong | 11.483 ms | 0.0226 ms | 0.0177 ms | 2.85 | 0.01 | 9687.5 | – | 41,824,022 B |
MvvmCrossWeak | 13.941 ms | 0.1865 ms | 0.1744 ms | 3.47 | 0.04 | 9687.5 | – | 41,824,007 B |
MVVMLight | 52.929 ms | 0.1295 ms | 0.1011 ms | 13.14 | 0.06 | 7600 | – | 33,120,010 B |
Stylet | 91.540 ms | 0.6362 ms | 0.4967 ms | 22.73 | 0.17 | 35500 | – | 153,152,352 B |
MvvmGen | 141.743 ms | 2.7249 ms | 2.7983 ms | 35.31 | 0.7 | 19250 | – | 83,328,348 B |
Catel | 148.867 ms | 2.6825 ms | 2.5093 ms | 36.94 | 0.64 | 5250 | – | 22,736,316 B |
Prism | 150.077 ms | 0.5359 ms | 0.4184 ms | 37.26 | 0.13 | 17500 | 250 | 76,096,900 B |
CaliburnMicro | 280.740 ms | 3.7625 ms | 3.1418 ms | 69.74 | 0.82 | 88000 | 2000 | 381,859,608 B |
MauiMessagingCenter | 673.656 ms | 1.7619 ms | 1.3755 ms | 167.26 | 0.63 | 8000 | – | 35,588,776 B |
每个基准测试运行涉及向 100 个收件人发送 4 条不同的消息 1000 次。如您所见,WeakReferenceMessenger 和 StrongReferenceMessenger 都是迄今为止最快的,也是唯一一个在广播消息时甚至不分配一个字节的 。
公共 DependentHandle API:
https://github.com/dotnet/runtime/pull/54246
改进的集合 API
这个新版本的 MVVM 工具包 还将所有可观察的分组集合类型从 CommunityToolkit.Common 包移动到 CommunityToolkit.Mvvm,同时还进行了一些重大更改以改进 API 表面并使其在更多场景中有用。这些 API 在处理分组项目时特别有用(例如,显示联系人列表),它们现在还包括扩展以极大地促进常见操作,例如在组内的正确位置插入项目(使用默认比较器 或输入一个,并在需要时创建一个新组)。
这个视频,展示了来自 MVVM Toolkit 示例应用程序的简单联系人视图:
宣布 MVVM 工具包示例应用程序
为了配合新版本,我们还在 Microsoft Store 中发布了示例应用程序!它包括 MS Docs 上可以找到的所有文档,以及许多可用 API 的交互式示例。它旨在成为 MVVM 工具包的伴侣,我们希望它能帮助人们开始使用这个库,从而更加熟悉它!
从 Microsoft Store 下载并试用!
Microsoft Store:
https://apps.microsoft.com/store/detail/9NKLCF1LVZ5H?hl=zh-cn&gl=CN
改进的诊断 API
CommunityToolkit.Diagnostics 包也获得了一些新的改进,利用了新的 C# 10 内插字符串处理程序和调用者参数表达式功能。一些以前接受字符串的 Guard API 现在也接受自定义处理程序,允许调用站点在没有抛出异常的情况下完全跳过插值步骤,而且也不再需要手动指示参数名称。
这是一个快速的前后比较:
// 诊断 7.1
public static void SampleMethod(int[] array, int index, Span<int> span, string text)
{Guard.IsNotNull(array, nameof(array));Guard.HasSizeGreaterThanOrEqualTo(array, 10, nameof(array));Guard.IsInRangeFor(index, array, nameof(index));Guard.HasSizeLessThanOrEqualTo(array, span, nameof(span));Guard.IsNotNullOrEmpty(text, nameof(text));
}
// 诊断 8.0
public static void SampleMethod(int[] array, int index, Span<int> span, string text)
{Guard.IsNotNull(array);Guard.HasSizeGreaterThanOrEqualTo(array, 10);Guard.IsInRangeFor(index, array);Guard.HasSizeLessThanOrEqualTo(array, span);Guard.IsNotNullOrEmpty(text);
}
C# 10 内插字符串处理程序
https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/interpolated-string-handler?ocid=AID3052907
调用者参数表达式
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/caller-argument-expression?ocid=AID3052907
.NET 6 支持
这个新版本的 .NET 社区工具包还增加了对 .NET 6 的支持,将其作为所有可用库的新目标。在最新的 .NET 运行时上运行时,带来了一些改进:
现在为所有库启用了修剪支持。为了支持这一点,所有包还为所有 API 提供了完整的修整注释,以确保所有内容要么对链接器友好,要么在编译时显式显示正确的警告(例如,MVVM 工具包中的某些验证 API 就是这种情况 ,它们使用 BCL 中的一些 API,这些 API 本质上需要一些反射才能工作)。
High Performance 包中的 Count<T>() 扩展现在也支持 mint 和 nunit。
为 .NET 6 上的所有包引入了其他几项优化。
当然,所有库都将继续支持 .NET Standard 2.0,因此您也可以继续从具有不同目标框架的项目中引用它们。由于 NuGet 包解析的工作原理,如果您使用这些包和较低目标框架(例如 .NET Standard 2.0)编写库,并且消费者从针对较新 .NET 版本的项目中引用它(例如.NET 6),他们仍然会自动获得可用的 .NET 社区工具包 程序集的最优化版本!
在这个新版本中还有更多内容!
您可以在 GitHub 发布页面上查看完整的变更日志。
您可以在我们的 GitHub 存储库中找到所有源代码,在MS Docs 网站上找到一些手写文档,在 .NET API 浏览器网站上找到完整的 API 参考。如果您想做出贡献,请随时提出问题或联系我们,让我们了解您的体验!要关注 Twitter 上的对话,请使用 #CommunityToolkit 标签。您的所有反馈都极大地帮助了这些库的发展方向,因此请务必分享它们!
GitHub 存储库
https://github.com/CommunityToolkit/dotnet
MS Docs 网站
https://docs.microsoft.com/en-us/dotnet/communitytoolkit/?ocid=AID3052907
谢谢你读完了本文!欢迎在评论区留言分享你的想法,并且转发到朋友圈。
长按识别二维码
关注微软开发者MSDN
点击「阅读原文」了解.NET社区工具包~