CommunityToolkit.Mvvm 架构笔记:现代 MVVM、源生成器与工程化实践
一、核心概念术语说明CommunityToolkit.Mvvm微软维护的现代 MVVM 工具包适合新项目和旧项目迁移ObservableObject最基础的可通知对象封装INotifyPropertyChanged与INotifyPropertyChangingObservableRecipient在ObservableObject基础上增加消息接收能力适合需要订阅消息的 ViewModelRelayCommand同步命令实现适合按钮点击、菜单切换等操作AsyncRelayCommand异步命令实现适合数据库、网络、文件等耗时操作WeakReferenceMessenger基于弱引用的消息总线减少订阅对象因忘记注销而导致的内存泄漏风险ObservableProperty源生成器特性通过字段标记自动生成属性与通知逻辑RelayCommand 特性源生成器特性通过方法标记自动生成命令属性Ioc.DefaultToolkit 提供的轻量 DI 访问入口常与Microsoft.Extensions.DependencyInjection一起使用但不是必选项源生成器编译期自动生成样板代码减少手写属性、命令与通知逻辑如果把 MVVM Light 看作“传统 MVVM 教学版”那么 CommunityToolkit.Mvvm 更像“现代 MVVM 工程版”保留核心模式但尽量用编译期生成替代重复模板代码。二、常用操作常用能力速查操作/能力常见类型或语法说明属性通知ObservableObject、SetProperty(...)基础属性变更通知自动生成属性[ObservableProperty]通过字段生成完整属性与通知代码同步命令RelayCommand、[RelayCommand]绑定普通按钮和菜单操作异步命令AsyncRelayCommand、[RelayCommand] async Task绑定异步业务流程消息通信WeakReferenceMessenger、ObservableRecipient用于跨模块解耦通知依赖注入Ioc.DefaultServiceCollection用于注册服务和 ViewModel输入校验ObservableValidator在 ViewModel 中组合数据校验能力1. 安装与基础使用// 安装 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 属性首选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. 自动生成命令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 响应性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 IReadOnlyListUserDto users Array.EmptyUserDto(); }说明在现代 MVVM 项目中数据库、HTTP、文件、串口等耗时任务都更适合放入异步命令。AsyncRelayCommand能避免把耗时逻辑直接塞进同步命令造成 UI 假死。这也是 CommunityToolkit.Mvvm 相比老框架更贴近现代 .NET 开发习惯的地方。5. 使用弱引用消息通信using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging.Messages; namespace Demo.Messages; public sealed class LoginUserChangedMessage : ValueChangedMessageUserDto { 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.RegisterLoginUserChangedMessage(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 注册using CommunityToolkit.Mvvm.DependencyInjection; using Microsoft.Extensions.DependencyInjection; var services new ServiceCollection(); services.AddSingletonIUserService, UserService(); services.AddTransientLoginViewModel(); services.AddTransientMainViewModel(); services.AddTransientUserListViewModel(); Ioc.Default.ConfigureServices(services.BuildServiceProvider()); using CommunityToolkit.Mvvm.DependencyInjection; using System.Windows; public partial class LoginWindow : Window { public LoginWindow() { InitializeComponent(); DataContext Ioc.Default.GetRequiredServiceLoginViewModel(); } }说明这一模式可视为对 MVVM Light 中SimpleIoc ViewModelLocator的现代替代。现在更常见的做法不是继续依赖ServiceLocator风格而是使用Microsoft.Extensions.DependencyInjection统一管理注册与解析。对 WPF 而言即使不强制上 ASP.NET Core 那套完整宿主也可以单独使用ServiceCollection。Ioc.Default是方便访问容器的入口但不是必须使用如果项目已有自己的宿主或容器组织方式也可以直接使用现有 DI 体系。7. 表单校验与可观察验证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 LightCommunityToolkit.Mvvm说明ViewModelBaseObservableObject/ObservableRecipient是否需要消息接收决定选哪个基类RelayCommandRelayCommand/AsyncRelayCommandToolkit 补足了更现代的异步命令体验MessengerWeakReferenceMessenger推荐使用弱引用消息总线SimpleIocIoc.DefaultServiceCollection更推荐与Microsoft.Extensions.DependencyInjection协同手写RaisePropertyChanged[ObservableProperty]源生成器减少模板代码手写命令属性[RelayCommand]方法即命令编译期生成命令成员9. 在 WPF 实际项目中的推荐组织方式// 目录示例 // 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 包正常安装并重新生成项目。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。WeakReferenceMessenger.Default.RegisterLoginUserChangedMessage(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 习惯大幅减少样板代码。