我们很高兴地宣布新的.NET社区工具包的正式发布,它已经在NuGet上发布了8.0.0版本这是一个重要的版本,包括大量的新功能、改进、优化、错误修复和许多重构,以反映新的项目结构和组织,这篇博文将详细描述。
与每个社区工具包的发布一样,所有的变化都受到了微软使用工具包的团队以及社区中其他开发者的反馈的影响。我们非常感谢每一个做出贡献的人,他们一直在帮助.NET社区工具包变得更好。
.NET社区工具包里有什么?
.NET社区工具包是一个帮助程序和API的集合,它适用于所有的.NET开发者,并且与任何特定的UI平台无关。该工具包由微软维护和发布,是.NET基金会的一部分。它也被一些内部项目和收件箱应用所使用,如微软商店。从新的8.0.0版本开始,该项目现在在GitHub上的CommunityToolkit/dotnet存储库中,其中包括属于工具包的所有库。
所有可用的API都不依赖于任何特定的运行时或框架,所以它们可以被所有的.NET开发者使用。这些库从.NET标准2.0到.NET 6都是多目标的,因此它们既能支持尽可能多的平台,又能在较新的运行时进行优化以获得最佳性能。
.NET社区工具包中的库包括。
CommunityToolkit.CommonCommunityToolkit.Mvvm(又名 "微软MVVM工具包")CommunityToolkit.DiagnosticsCommunityToolkit.HighPerformance
一段小的历史
你可能想知道为什么.NET社区工具包的第一个版本是8.0.0版本。 好问题!原因是.NET社区工具包的所有库最初都是Windows社区工具包的一部分,它是一个帮助程序、扩展程序和自定义控件的集合,它简化并演示了为Windows 10和Windows 11构建UWP和.NET应用程序的常见开发者任务。
随着时间的推移,仅仅针对.NET且没有任何Windows特定依赖的API数量不断增加,我们决定将它们拆分为一个单独的项目,以便它们可以独立发展,并且对于不做任何Windows开发的.NET开发人员来说,也更容易找到。这就是.NET社区工具包的诞生过程。这也使我们更容易更好地组织文档,现在每个特定平台的工具包都有单独的部分。
由于Windows Community Toolkit在分支之前的最后一个版本是7.1.x,我们决定遵循这个语义上的版本号,以使现有用户更容易理解过渡,这就是为什么.NET Community Toolkit的第一个版本是8.0.0。
澄清了这一点,现在让我们深入了解.NET社区工具包库的这个新的主要版本中的所有新功能吧。
MVVM工具箱
正如之前在7.0版本中所宣布的,.NET社区工具包的主要组成部分之一是MVVM工具包:一个现代的、快速的、平台无关的、模块化的MVVM库。这也是微软商店、照片应用等使用的MVVM库
MVVM工具包的灵感来自于MvvmLight,也是它的官方替代品,因为该库已被废弃。在开发MVVM工具包时,我们与Laurent Bugnion进行了合作,他也赞同将MVVM工具包作为现有MvvmLight用户的发展之路(我们也有这方面的迁移文档)。
MVVM工具包是建立在几个关键原则之上的:
-
平台无关性:意味着它不依赖于任何特定的UI框架。你可以用它来分享UWP、WinUI 3、MAUI、WPF、Avalonia、Uno等的代码。
-
运行时不可知性:该库多目标并支持低至.NET标准2.0,这意味着你可以在现代运行时(如.NET 6)获得性能改进,以及即使在.NET框架上也能使用它。
-
简单易用:对应用程序的结构或编码模式没有严格的要求。你可以使用该库来适应你自己的结构和风格。
-
点菜:所有组件都是独立的,也可以单独使用。没有强迫你采用 "全盘接受 "的方式:如果你只想使用整个库中的单一类型,你可以做到这一点,然后根据需要逐渐开始使用更多的功能。
-
参考实现:所有可用的API都是为了精简和执行,为包含在.NET基类库中的接口提供 "参考实现",但缺乏直接使用它们的具体类型。例如,你能够为诸如
INotifyPropertyChanged或ICommand.NET的接口找到一个 "参考实现"。
MVVM工具包的源生成器
MVVM工具包8.0.0版本中最大的新功能是新的MVVM源码生成器,它旨在大大减少使用MVVM设置应用程序所需的模板代码。与我们在7.1.0发布的预览版生成器相比,它们也被完全重写为增量生成器,这意味着它们的运行速度将比以前快得多,即使在处理大规模项目时,它们也将帮助保持IDE的快速和响应。
你可以在这里找到我们关于新的源码生成器的所有文档,如果你喜欢视频版本,James Montemagno也做了几个关于它们的视频,比如这一个。让我们再来看看MVVM工具包中由源码生成器驱动的主要功能。
命令
创建命令可能是相当重复的,我们需要为每一个方法设置一个属性,以抽象的方式暴露给我们应用程序中要调用它们的各种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 方法,而且还可以控制异步命令的并发水平。还有一些额外的选项可以对生成的命令的行为进行微调,你可以在我们的文档中了解更多信息。
可观察的属性
编写可观察的属性可能非常冗长,特别是当还需要添加额外的逻辑来处理被通知的依赖属性时。现在,通过使用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 Toolkit现在也会生成两个没有实现的部分方法:On<PROPERTY_NAME>Changing 和On<PROPERTY_NAME>Changed 。这些方法可用于在属性发生变化时注入额外的逻辑,而不需要回退到使用手动属性。请注意,由于这两个方法是局部的、无效返回的、没有定义的,如果它们没有被实现,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] 还有更多的功能,就像命令一样,你可以在我们的文档中阅读更多关于它们的信息和看到更多的例子。
对命令的取消支持
[RelayCommand] 属性增加了一个新的属性,它可以用来指示源生成器在生成原始命令的同时生成一个取消命令。这个取消命令可以用来取消一个异步命令的执行。
这也展示了[RelayCommand] 如何自动适应异步方法和也接受参数的方法,并在幕后创建异步命令的实现。这也实现了额外的功能,如易于设置的绑定,以显示进度指示器,以及其他的功能。
这是一个如何使用它们的例子:
[RelayCommand(IncludeCancelCommand = true)]
private async Task DoWorkAsync(CancellationToken token)
{
// Do some long running work with cancellation support
}
从这个小片段中,生成器将产生以下代码:
private AsyncRelayCommand? doWorkCommand;
public IAsyncRelayCommand DoWorkCommand => doWorkCommand ??= new AsyncRelayCommand(DoWorkAsync);
ICommand? doWorkCancelCommand;
public ICommand DoWorkCancelCommand => doWorkCancelCommand ??= IAsyncRelayCommandExtensions.CreateCancelCommand(UpdateSomethingCommand);
这个生成的代码,结合IAsyncRelayCommandExtensions.CreateCancelCommand API中的逻辑,允许你只需要一行代码就可以生成一个命令,每当工作开始或正在运行时通知用户界面,并自动进行并发控制(当命令已经运行时,默认是禁用的)。当主命令开始或结束运行时,单独的取消命令将被通知,当执行时将向传递给主命令所包裹的方法的令牌发出取消信号。所有这些,都被完全抽象化了,只需一个属性就能轻松访问。
对生成的属性的广播变化支持
我们还添加了一个新的[NotifyPropertyChangedRecipients] 属性,它可以用于从ObservableRecipient 继承的类型(或用[ObservableRecipient] 注释的类型)中生成的可观察属性。使用它将产生一个对Broadcast方法的调用,向所有其他订阅的组件发送关于刚刚发生的属性变化的消息。这在视图模型的属性变化需要通知应用程序中的其他组件的情况下非常有用(假设有一个IsLoggedIn布尔属性在用户登录时更新;这可以通知并触发应用程序中的一些其他组件用广播的消息刷新)。
它的使用方法如下:
[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();
}
}
}
这是增强生成的属性的另一个功能,并确保它们可以在几乎所有的场景中使用,而不会被迫退回到手动属性。
视图模型组成
C#没有多重继承,而这有时会妨碍到它。
如果有一个视图模型必须继承自一个特定的类型,但你也想注入INotifyPropertyChanged支持,或者让它也继承自ObservableRecipient以获得对其API的访问,该怎么办?
MVVM工具包现在包括了一种解决这个问题的方法,它为代码生成引入了属性,允许将这些类型的逻辑注入任意的类中。这些属性是[INotifyPropertyChanged],[ObservableObject] 和[ObservableRecipient] 。
将它们添加到一个类中,将导致MVVM Toolkit源码生成器将该类型的所有逻辑纳入该类,就像该类也继承了该类型一样。比如说:
[INotifyPropertyChanged]
partial class MyObservableViewModel : DatabaseItem
{
}
这个MyObservableViewModel 将继承自DatabaseItem ,正如你所期望的那样,但是使用[INotifyPropertyChanged] 将使它也获得对INotifyPropertyChanged 的支持,以及ObservableObject 自己所包含的所有帮助性API。
我们仍然建议在需要的时候继承自基础类型,如ObservableObject ,因为这也有助于减少二进制的大小,但在需要的时候,有能力以这种方式注入代码可以帮助解决C#的限制,在改变视图模型的基础类型是不可能的,如上面的例子中。
改进的信使API
MVVM工具包中另一个常用的功能是IMessenger 接口,它是一个可用于在不同对象之间交换消息的类型契约。
这对于解耦应用程序的不同模块非常有用,而不必对被引用的类型保持强引用。也可以将消息发送到特定的通道,由一个令牌唯一标识,并在应用程序的不同部分有不同的信使。
MVVM工具包提供了这个接口的两种实现:
WeakReferenceMessenger采集器:它不对接收者进行根控,并允许他们被采集。这是通过依赖句柄实现的,它是一种特殊类型的GC引用,允许这个信使确保总是允许注册的接收者被收集,即使有注册的处理程序引用他们,但没有其他突出的强引用存在。StrongReferenceMessenger这就是一个信使的实现,将已注册的接收者扎根,以确保它们保持活力,即使信使是唯一引用它们的对象。
下面是一个关于如何使用这个接口的小例子:
// Declare a message
public sealed record LoggedInUserChangedMessage(User user);
// Register a recipient explicitly...
messenger.Register<MyViewModel, LoggedInUserChangedMessage>(this, static (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.
});
// ...or have the viewmodel implement IRecipient<TMessage>...
class MyViewModel : IRecipient<LoggedInUserChangedMessage>
{
public void Receive(LoggedInUserChangedMessage message)
{
// Handle the message here
}
}
// ...and then register through the interface (other APIs are available too)
messenger.Register<LoggedInuserChangedMessage>(this);
// Send a message from some other module
messenger.Send(new LoggedInUserChangedMessage(user));
由于有了新的公共DependentHandle API,MVVM工具包新版本中的信使实现在.NET 6中得到了高度优化,这使得信使类型既能变得比以前更快,也能提供完全零分配的消息广播。下面是一些基准测试,显示了MVVM工具包中的信使与其他广泛使用的MVVM库中的其他同等类型的信使的对比情况。
| 方法 | 平均值 | 误差 | StdDev | 比值 | 比率SD | 0代 | 第1代 | 已分配 |
|---|---|---|---|---|---|---|---|---|
| MVVMToolkitStrong | 4.025 ms | 0.0177 ms | 0.0147 ms | 1.00 | 0.00 | - | - | - |
| 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.5000 | - | 41,824,022 B |
| MvvmCrossWeak | 13.941 ms | 0.1865 ms | 0.1744 ms | 3.47 | 0.04 | 9687.5000 | - | 41,824,007 B |
| 视觉效果图(MVVMLight | 52.929 ms | 0.1295 ms | 0.1011 ms | 13.14 | 0.06 | 7600.0000 | - | 33,120,010 B |
| 风格 | 91.540 ms | 0.6362 ms | 0.4967 ms | 22.73 | 0.17 | 35500.0000 | - | 153,152,352 B |
| モンクレールマップメーカーション | 141.743 ms | 2.7249 ms | 2.7983 ms | 35.31 | 0.70 | 19250.0000 | - | 83,328,348 B |
| 卡特尔 | 148.867毫秒 | 2.6825 ms | 2.5093毫秒 | 36.94 | 0.64 | 5250.0000 | - | 22,736,316 B |
| 棱镜 | 150.077毫秒 | 0.5359 ms | 0.4184毫秒 | 37.26 | 0.13 | 17500.0000 | 250.0000 | 76,096,900 B |
| 卡利本微 | 280.740毫秒 | 3.7625 ms | 3.1418毫秒 | 69.74 | 0.82 | 88000.0000 | 2000.0000 | 381,859,608 B |
| 茂名消息中心 | 673.656 ms | 1.7619 ms | 1.3755 ms | 167.26 | 0.63 | 8000.0000 | - | 35,588,776 B |
每个基准运行包括向100个接收者发送4个不同的信息1000次。正如你所看到的,WeakReferenceMessenger 和StrongReferenceMessenger 到目前为止都是最快的,而且是唯一在广播消息时不分配任何一个字节的。
改良后的集合API
MVVM工具包的这个新版本还将所有可观察的分组集合类型从CommunityToolkit.Common 包中移到了CommunityToolkit.Mvvm ,同时还做了一些重大改变,以改善API表面,使其在更多的场景中发挥作用。这些API在处理分组项目时特别有用(例如,显示联系人列表),它们现在还包括一些扩展,大大方便了常见的操作,例如在组内的正确位置插入一个项目(使用默认的比较器或输入的比较器,如果需要,还可以创建一个新组)。
这里有一个GIF,展示了MVVM工具包示例应用程序中一个简单的联系人视图。
宣布MVVM工具包示例应用程序
为了配合新版本的发布,我们还在微软商店中发布了示例应用程序!它包括所有的文档,也可以在微软商店中找到。它包括MS Docs上的所有文档,以及许多可用API的交互式样本。它旨在成为MVVM工具包的伴侣,我们希望它能帮助刚开始使用这个库的人更加熟悉它!
请从微软商店下载并试用它吧!
改进的诊断API
CommunityToolkit.Diagnostics 包也得到了一些新的改进,利用了新的C# 10插值字符串处理程序和调用者参数表达功能。几个Guard APIs以前采取string ,现在也接受自定义处理程序,如果没有抛出异常,允许调用者完全跳过插值步骤,而且也不再需要手动指示参数名称。
下面是一个快速的前后对比:
// Diagnostics 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));
}
// Diagnostics 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);
}
支持.NET 6
这个新发布的.NET社区工具包还增加了对.NET 6的支持,作为所有可用库的一个新目标。在最新的.NET运行时间上运行时,这带来了一些改进。
- 现在所有库都启用了修剪支持。为了支持这一点,所有的包都对所有的API进行了完整的修剪注释,以确保所有的东西都是链接器友好的,或者在编译时明确显示正确的警告(例如,MVVM工具包中的一些验证API就是这种情况,它们使用BCL中的一些API,这些API本质上需要一些反射才能工作)。
- HighPerformance包中的
Count<T>()扩展现在也支持nint和nuint。 - 在.NET 6上,所有包中的其他一些优化已经被引入。
当然,所有的库都会一直支持到.NET标准2.0,所以你也可以继续从不同目标框架的项目中引用它们。由于NuGet包的解析方式,如果你使用任何这些包和较低的目标框架(如.NET标准2.0)编写一个库,并且消费者从一个针对新的.NET版本(如.NET 6)的项目中引用它,他们仍然会自动获得最优化的.NET Community Toolkit程序集的版本。
其他变化!
在这个新版本中,还有很多东西被包含在其中。
你可以在GitHub的发布页面上看到完整的变化日志。
今天就开始吧!
你可以在GitHub repo中找到所有源代码,在MS Docs网站上找到一些手写的文档,在.NET API浏览器网站上找到完整的API参考。如果你想做出贡献,请随时打开问题或联系我们,让我们了解你的经验要在Twitter上关注这一对话,请使用#CommunityToolkit标签。
编码愉快!


