CommunityToolkit.Mvvm 架构笔记:现代 MVVM、源生成器与工程化实践

一、核心概念

术语 说明
CommunityToolkit.Mvvm 微软维护的现代 MVVM 工具包,适合新项目和旧项目迁移
ObservableObject 最基础的可通知对象,封装 INotifyPropertyChangedINotifyPropertyChanging
ObservableRecipient ObservableObject 基础上增加消息接收能力,适合需要订阅消息的 ViewModel
RelayCommand 同步命令实现,适合按钮点击、菜单切换等操作
AsyncRelayCommand 异步命令实现,适合数据库、网络、文件等耗时操作
WeakReferenceMessenger 基于弱引用的消息总线,减少订阅对象因忘记注销而导致的内存泄漏风险
ObservableProperty 源生成器特性,通过字段标记自动生成属性与通知逻辑
RelayCommand 特性 源生成器特性,通过方法标记自动生成命令属性
Ioc.Default Toolkit 提供的轻量 DI 访问入口,常与 Microsoft.Extensions.DependencyInjection 一起使用,但不是必选项
源生成器 编译期自动生成样板代码,减少手写属性、命令与通知逻辑

如果把 MVVM Light 看作"传统 MVVM 教学版",那么 CommunityToolkit.Mvvm 更像"现代 MVVM 工程版":保留核心模式,但尽量用编译期生成替代重复模板代码。

二、常用操作

常用能力速查

操作/能力 常见类型或语法 说明
属性通知 ObservableObjectSetProperty(...) 基础属性变更通知
自动生成属性 [ObservableProperty] 通过字段生成完整属性与通知代码
同步命令 RelayCommand[RelayCommand] 绑定普通按钮和菜单操作
异步命令 AsyncRelayCommand[RelayCommand] async Task 绑定异步业务流程
消息通信 WeakReferenceMessengerObservableRecipient 用于跨模块解耦通知
依赖注入 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 中看不到 AccountPassword 等生成属性,绑定报错。

  • 原因 :类型没有声明为 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 习惯大幅减少样板代码。

相关推荐
token-go2 小时前
Means:基于 .NET 10 打造的开源自部署 S3 兼容对象存储服务
低代码·.net·rxjava
加号32 小时前
【C#】 JSON 序列化与反序列化:从入门到最佳实践
c#·json
上海云盾第一敬业销售3 小时前
高防CDN与传统CDN架构解析
web安全·架构·ddos
ZengLiangYi3 小时前
AI 编程工具的数据格式为什么不能统一
javascript·后端·架构
webmote334 小时前
从零打造虚拟小智:用浏览器模拟 IoT 设备的实践之路
aigc·.net·嵌入式
杉氧4 小时前
100% Kotlin:基于 KMP + Compose Multiplatform 的全栈架构实战(Clean Architecture + MVI)
android·架构
imDwAaY4 小时前
贝叶斯网络到粒子滤波Python算法实现 CS188 Proj4 学习笔记
网络·人工智能·笔记·python·学习·算法
杉氧4 小时前
第一篇:从一个 Dagger 报错开始:手把手带你搭建 Hilt 依赖注入的护城河
android·架构
自进化Agent智能体4 小时前
Hermes Trajectory日志工程:让每一次执行都成为进化数据
架构