一、核心概念
| 术语 | 说明 |
|---|---|
| CommunityToolkit.Mvvm | 微软维护的现代 MVVM 工具包,适合新项目和旧项目迁移 |
| ObservableObject | 最基础的可通知对象,封装 INotifyPropertyChanged 与 INotifyPropertyChanging |
| ObservableRecipient | 在 ObservableObject 基础上增加消息接收能力,适合需要订阅消息的 ViewModel |
| RelayCommand | 同步命令实现,适合按钮点击、菜单切换等操作 |
| AsyncRelayCommand | 异步命令实现,适合数据库、网络、文件等耗时操作 |
| WeakReferenceMessenger | 基于弱引用的消息总线,减少订阅对象因忘记注销而导致的内存泄漏风险 |
| ObservableProperty | 源生成器特性,通过字段标记自动生成属性与通知逻辑 |
| RelayCommand 特性 | 源生成器特性,通过方法标记自动生成命令属性 |
| Ioc.Default | Toolkit 提供的轻量 DI 访问入口,常与 Microsoft.Extensions.DependencyInjection 一起使用,但不是必选项 |
| 源生成器 | 编译期自动生成样板代码,减少手写属性、命令与通知逻辑 |
如果把 MVVM Light 看作"传统 MVVM 教学版",那么 CommunityToolkit.Mvvm 更像"现代 MVVM 工程版":保留核心模式,但尽量用编译期生成替代重复模板代码。
二、常用操作
常用能力速查
| 操作/能力 | 常见类型或语法 | 说明 |
|---|---|---|
| 属性通知 | ObservableObject、SetProperty(...) |
基础属性变更通知 |
| 自动生成属性 | [ObservableProperty] |
通过字段生成完整属性与通知代码 |
| 同步命令 | RelayCommand、[RelayCommand] |
绑定普通按钮和菜单操作 |
| 异步命令 | AsyncRelayCommand、[RelayCommand] async Task |
绑定异步业务流程 |
| 消息通信 | WeakReferenceMessenger、ObservableRecipient |
用于跨模块解耦通知 |
| 依赖注入 | Ioc.Default + ServiceCollection |
用于注册服务和 ViewModel |
| 输入校验 | ObservableValidator |
在 ViewModel 中组合数据校验能力 |
1. 安装与基础使用
cs
// 安装 NuGet 包
// CommunityToolkit.Mvvm
using CommunityToolkit.Mvvm.ComponentModel;
namespace Demo.ViewModels;
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
private string account = string.Empty;
[ObservableProperty]
private string password = string.Empty;
}
说明:
-
标记为
partial是因为源生成器会在编译期为当前类型补充成员。 -
[ObservableProperty]会根据字段名生成标准 PascalCase 属性,如account->Account。 -
生成的属性内部会自动调用通知逻辑,通常不需要再手写
SetProperty。
2. 手写属性通知与源生成器写法对比
| 方式 | 写法特点 | 适用场景 |
|---|---|---|
手写 SetProperty |
可读性直接、行为清晰 | 需要自定义细节较多时 |
[ObservableProperty] |
样板代码最少 | 常规 ViewModel 属性首选 |
cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace Demo.ViewModels;
public class CustomerViewModel : ObservableObject
{
private string name = string.Empty;
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
}
using CommunityToolkit.Mvvm.ComponentModel;
namespace Demo.ViewModels;
public partial class CustomerViewModel : ObservableObject
{
[ObservableProperty]
private string name = string.Empty;
}
典型收益:
-
减少重复字段/属性/通知模板代码。
-
更适合拥有大量表单字段、筛选条件、状态字段的 ViewModel。
-
相比 MVVM Light 手写
RaisePropertyChanged()更整洁。
3. 自动生成命令
cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks;
namespace Demo.ViewModels;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string currentPage = "Home";
[RelayCommand]
private void OpenPage(string pageName)
{
CurrentPage = pageName;
}
[RelayCommand]
private async Task LoadAsync()
{
await Task.Delay(300);
}
}
生成结果的理解方式:
-
OpenPage会生成一个可绑定的OpenPageCommand。 -
LoadAsync会生成一个可绑定的异步命令属性,适合在界面中直接绑定加载、刷新、保存等异步操作。 -
对于多数 CRUD、页面切换、搜索、保存、刷新操作,这种写法明显比传统手写命令更简洁。
4. 异步命令与 UI 响应性
cs
using CommunityToolkit.Mvvm.Input;
namespace Demo.ViewModels;
public partial class UserListViewModel : ObservableObject
{
private readonly IUserService userService;
public UserListViewModel(IUserService userService)
{
this.userService = userService;
RefreshUsersCommand = new AsyncRelayCommand(RefreshUsersAsync);
}
public IAsyncRelayCommand RefreshUsersCommand { get; }
private async Task RefreshUsersAsync()
{
Users = await userService.GetUsersAsync();
}
[ObservableProperty]
private IReadOnlyList<UserDto> users = Array.Empty<UserDto>();
}
说明:
-
在现代 MVVM 项目中,数据库、HTTP、文件、串口等耗时任务都更适合放入异步命令。
-
AsyncRelayCommand能避免把耗时逻辑直接塞进同步命令造成 UI 假死。 -
这也是 CommunityToolkit.Mvvm 相比老框架更贴近现代 .NET 开发习惯的地方。
5. 使用弱引用消息通信
cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Demo.Messages;
public sealed class LoginUserChangedMessage : ValueChangedMessage<UserDto>
{
public LoginUserChangedMessage(UserDto value) : base(value)
{
}
}
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
namespace Demo.ViewModels;
public partial class LoginViewModel : ObservableObject
{
[RelayCommand]
private void Login()
{
var currentUser = new UserDto { Account = Account };
WeakReferenceMessenger.Default.Send(new LoginUserChangedMessage(currentUser));
}
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
namespace Demo.ViewModels;
public partial class HeaderViewModel : ObservableRecipient
{
[ObservableProperty]
private string currentAccount = string.Empty;
public HeaderViewModel()
{
IsActive = true;
}
protected override void OnActivated()
{
WeakReferenceMessenger.Default.Register<LoginUserChangedMessage>(this, static (recipient, message) =>
{
((HeaderViewModel)recipient).CurrentAccount = message.Value.Account;
});
}
}
与 MVVM Light 的差异点:
-
MVVM Light 常用
Messenger.Default。 -
Toolkit 推荐
WeakReferenceMessenger.Default。 -
使用
ObservableRecipient时,需要确保对象已进入激活状态,例如设置IsActive = true,这样OnActivated()中的注册逻辑才会执行。 -
弱引用设计可以降低忘记取消注册带来的内存泄漏风险。
6. 依赖注入与 ViewModel 注册
cs
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<IUserService, UserService>();
services.AddTransient<LoginViewModel>();
services.AddTransient<MainViewModel>();
services.AddTransient<UserListViewModel>();
Ioc.Default.ConfigureServices(services.BuildServiceProvider());
using CommunityToolkit.Mvvm.DependencyInjection;
using System.Windows;
public partial class LoginWindow : Window
{
public LoginWindow()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<LoginViewModel>();
}
}
说明:
-
这一模式可视为对 MVVM Light 中
SimpleIoc + ViewModelLocator的现代替代。 -
现在更常见的做法不是继续依赖
ServiceLocator风格,而是使用Microsoft.Extensions.DependencyInjection统一管理注册与解析。 -
对 WPF 而言,即使不强制上 ASP.NET Core 那套完整宿主,也可以单独使用
ServiceCollection。 -
Ioc.Default是方便访问容器的入口,但不是必须使用;如果项目已有自己的宿主或容器组织方式,也可以直接使用现有 DI 体系。
7. 表单校验与可观察验证
cs
using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Demo.ViewModels;
public partial class RegisterViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "账号不能为空")]
[MinLength(4, ErrorMessage = "账号长度不能小于 4")]
private string account = string.Empty;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors)
{
return;
}
// 保存逻辑
}
}
适用场景:
-
登录、注册、筛选条件、设备参数配置等表单型界面。
-
比单纯在命令中
if/else判断更利于统一管理验证规则。
8. 从 MVVM Light 迁移时的类型映射
| MVVM Light | CommunityToolkit.Mvvm | 说明 |
|---|---|---|
ViewModelBase |
ObservableObject / ObservableRecipient |
是否需要消息接收,决定选哪个基类 |
RelayCommand |
RelayCommand / AsyncRelayCommand |
Toolkit 补足了更现代的异步命令体验 |
Messenger |
WeakReferenceMessenger |
推荐使用弱引用消息总线 |
SimpleIoc |
Ioc.Default + ServiceCollection |
更推荐与 Microsoft.Extensions.DependencyInjection 协同 |
手写 RaisePropertyChanged |
[ObservableProperty] |
源生成器减少模板代码 |
| 手写命令属性 | [RelayCommand] |
方法即命令,编译期生成命令成员 |
9. 在 WPF 实际项目中的推荐组织方式
html
// 目录示例
// Models/
// Services/
// ViewModels/
// Views/
// Messages/
// Converters/
// Behaviors/
// 推荐原则
// 1. 业务状态放 ViewModel。
// 2. 数据访问放 Service/Repository。
// 3. 页面切换通过导航服务或状态属性,而不是大量反射拼接字符串。
// 4. 跨模块通知使用消息对象,不直接互相持有引用。
// 5. 重复属性和命令优先交给源生成器处理。
三、问题排查
错误1:使用 [ObservableProperty] 后没有生成属性
-
现象 :编译通过前或 IDE 中看不到
Account、Password等生成属性,绑定报错。 -
原因 :类型没有声明为
partial,或者项目没有正确引用CommunityToolkit.Mvvm。 -
解决 :将类改为
partial,确认 NuGet 包正常安装并重新生成项目。
cs
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
private string account = string.Empty;
}
错误2:异步按钮点击后界面卡顿
-
现象:点击"加载""查询""登录"等按钮后窗口短暂无响应。
-
原因:仍在同步命令中直接执行耗时操作。
-
解决 :改为
AsyncRelayCommand或[RelayCommand] async Task,将 I/O 操作异步化。
错误3:消息接收器似乎没有收到消息
-
现象 :发送端调用
WeakReferenceMessenger.Default.Send(...)后,接收端没有更新。 -
原因 :接收端没有注册消息,或者
ObservableRecipient未激活。 -
解决 :显式注册消息;如果继承
ObservableRecipient,需要确保对象已激活,例如设置IsActive = true。
cs
WeakReferenceMessenger.Default.Register<LoginUserChangedMessage>(this, static (recipient, message) =>
{
((HeaderViewModel)recipient).CurrentAccount = message.Value.Account;
});
错误4:从 MVVM Light 直接复制 ViewModelLocator 用法后结构变得混乱
-
现象 :新项目一边写
ViewModelLocator,一边又使用ServiceCollection,注册入口分散。 -
原因:把旧框架习惯原封不动搬到新框架中。
-
解决 :优先统一到
Ioc.Default + ServiceCollection或更完整的宿主启动模式,不必强依赖传统 Locator 风格。
错误5:表单验证规则写了但界面没有提示
-
现象 :
[Required]等特性存在,但保存时没有触发错误状态。 -
原因 :没有调用
ValidateAllProperties(),或属性未启用错误通知相关特性,或界面没有正确绑定验证错误显示。 -
解决 :在提交命令中先校验,并保证使用
ObservableValidator、相关验证特性,以及界面的验证绑定配置正确。
如果你的目标是"日常项目长期使用 + 博客输出 + 现代 .NET 客户端开发",CommunityToolkit.Mvvm 通常比 MVVM Light 更值得作为主力方案:一方面保留了 MVVM 的核心抽象,另一方面用源生成器、异步命令和现代 DI 习惯大幅减少样板代码。