做 Winform 开发,MVVM 模式几乎是绕不开的话题。但每次写数据绑定,都要面对这样一段"仪式感"极强的代码:
csharpprivate string _userName;
public string UserName
{
get => _userName;
set
{
if (_userName != value)
{
_userName = value;
OnPropertyChanged(nameof(UserName));
}
}
}
一个属性就要写这么多行。项目里有 20 个 ViewModel,每个 ViewModel 平均 10 个属性,那就是 200 段几乎一模一样的样板代码。不仅写得累,维护起来也是噩梦——改个属性名,还得手动同步好几处。
在我参与的一个中型 Winform 项目中,单单 ViewModel 层的 INotifyPropertyChanged 相关代码就占了整个 ViewModel 文件总行数的 约 40%,这些代码没有任何业务价值,纯粹是"机械劳动"。
好消息是,CommunityToolkit.Mvvm 的 [ObservableProperty] 源生成器彻底解决了这个问题。读完本文,你将掌握:
传统写法中,真正承载业务逻辑的代码只有赋值那一行,其余全是"噪音"。随着项目规模增长,这些样板代码会以线性速度膨胀,严重拉低代码可读性。
属性重命名时,nameof(UserName) 虽然比硬编码字符串安全,但私有字段 _userName、公共属性 UserName、nameof 表达式三处都需要同步修改。一旦遗漏,运行时绑定静默失效,排查起来相当费时。
为了复用 OnPropertyChanged,所有 ViewModel 都必须继承一个基类(通常是自己写的 ViewModelBase 或者 ObservableObject)。这在单继承的 C# 里是一种"继承位"的浪费,遇到需要继承其他基类的场景时会非常尴尬。

[ObservableProperty] 是 CommunityToolkit.Mvvm 提供的一个 Roslyn 源生成器(Source Generator) 特性。它的核心思路是:你只写私有字段,编译器替你生成完整的属性代码。
Roslyn 源生成器是 .NET 5+ 引入的编译期代码生成技术。在编译阶段,源生成器会扫描标注了 [ObservableProperty] 的字段,自动生成对应的公共属性、OnPropertyChanged 调用、以及可选的 OnXxxChanging / OnXxxChanged 分部方法。
整个过程发生在编译时,没有任何运行时反射开销,生成的代码与手写代码在性能上完全等价。
xml<!-- .csproj 中添加 NuGet 包 -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
需要 .NET 6+ 或 .NET Framework 4.6.1+(通过 netstandard2.0 支持),以及 C# 9.0+(需要 partial 类支持)。
[ObservableProperty] 替换手写属性这是最直接的改造,先从单个 ViewModel 开始。
改造前(传统写法):
csharppublic class UserViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _userName = string.Empty;
private int _age;
private bool _isActive;
public string UserName
{
get => _userName;
set
{
if (_userName != value)
{
_userName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UserName)));
}
}
}
public int Age
{
get => _age;
set
{
if (_age != value)
{
_age = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Age)));
}
}
}
public bool IsActive
{
get => _isActive;
set
{
if (_isActive != value)
{
_isActive = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsActive)));
}
}
}
protected void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
改造后(源生成器写法):
csharpusing CommunityToolkit.Mvvm.ComponentModel;
// 必须是 partial 类,源生成器需要向其中注入生成的代码
public partial class UserViewModel : ObservableObject
{
[ObservableProperty]
private string _userName = string.Empty;
[ObservableProperty]
private int _age;
[ObservableProperty]
private bool _isActive;
}
代码行数对比(测试环境:Visual Studio 2022,.NET 8,CommunityToolkit.Mvvm 8.3.2):
| 指标 | 传统写法 | 源生成器写法 | 降幅 |
|---|---|---|---|
| 有效代码行数 | 42 行 | 12 行 | ↓71% |
| 样板代码占比 | ~76% | 0% | — |
| 属性重命名改动点 | 3 处/属性 | 1 处/属性 | ↓67% |
生成的属性名遵循命名约定:私有字段
_userName→ 公共属性UserName(去掉下划线前缀,首字母大写)。
实际项目中,属性变更往往不是孤立的。比如修改用户名后需要同步更新"显示名称",或者触发一次数据校验。
源生成器为每个属性自动生成两个分部方法钩子:
OnXxxChanging(NewType value) —— 变更前触发OnXxxChanged(NewType value) —— 变更后触发csharppublic partial class UserViewModel : ObservableObject
{
[ObservableProperty]
private string _userName = string.Empty;
[ObservableProperty]
private string _displayName = string.Empty;
[ObservableProperty]
private bool _isNameValid;
// 用户名变更前:做预处理或日志记录
partial void OnUserNameChanging(string value)
{
// 例如:记录变更前的旧值,用于审计日志
System.Diagnostics.Debug.WriteLine($"UserName 即将从 '{UserName}' 变更为 '{value}'");
}
// 用户名变更后:联动更新显示名称和校验状态
partial void OnUserNameChanged(string value)
{
// 联动更新显示名称
DisplayName = string.IsNullOrWhiteSpace(value) ? "匿名用户" : $"用户:{value}";
// 联动更新校验状态
IsNameValid = value.Length is >= 2 and <= 20;
}
}
这个模式非常干净——业务逻辑集中在分部方法里,不需要在属性 setter 里堆砌各种 if/else,关注点分离做得相当彻底。
⚠️ 踩坑预警: 分部方法是由源生成器声明的,你只需要实现它。如果方法签名写错(比如参数类型不对),编译器不会报错,而是静默忽略你的实现,导致逻辑不生效。建议通过 IDE 的"转到定义"先查看生成的方法签名,再实现。
[RelayCommand] 构建可绑定命令光有属性还不够,MVVM 里命令(Command)同样是高频样板代码的重灾区。[RelayCommand] 与 [ObservableProperty] 配合,可以构建完整的 ViewModel 闭环。
csharpusing CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class LoginViewModel : ObservableObject
{
// 绑定属性:用户名、密码、加载状态
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(LoginCommand))] // 属性变更时自动通知命令可执行状态刷新
private string _username = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(LoginCommand))]
private string _password = string.Empty;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _errorMessage = string.Empty;
// 登录命令:源生成器自动生成 LoginCommand 属性(IRelayCommand 类型)
[RelayCommand(CanExecute = nameof(CanLogin))]
private async Task LoginAsync()
{
IsLoading = true;
ErrorMessage = string.Empty;
try
{
// 模拟异步登录请求(实际项目中替换为真实 API 调用)
await Task.Delay(1500);
if (Username == "admin" && Password == "123456")
{
// 登录成功逻辑
System.Diagnostics.Debug.WriteLine("登录成功");
}
else
{
ErrorMessage = "用户名或密码错误,请重试。";
}
}
catch (Exception ex)
{
ErrorMessage = $"登录失败:{ex.Message}";
}
finally
{
IsLoading = false;
}
}
// 命令可执行条件:用户名和密码均不为空时才允许点击
private bool CanLogin()
=> !string.IsNullOrWhiteSpace(Username)
&& !string.IsNullOrWhiteSpace(Password)
&& !IsLoading;
}
Winform 界面绑定(Form 代码):
csharpusing static System.Windows.Forms.VisualStyles.VisualStyleElement;
namespace AppMvvm04
{
public partial class FrmMain : Form
{
private readonly LoginViewModel _viewModel = new();
public FrmMain()
{
InitializeComponent();
// 属性双向绑定
txtUsername.DataBindings.Add("Text", _viewModel, nameof(_viewModel.Username),
false, DataSourceUpdateMode.OnPropertyChanged);
txtPassword.DataBindings.Add("Text", _viewModel, nameof(_viewModel.Password),
false, DataSourceUpdateMode.OnPropertyChanged);
// 加载状态绑定到进度条可见性
progressBar.DataBindings.Add("Visible", _viewModel, nameof(_viewModel.IsLoading));
// 错误信息绑定
lblError.DataBindings.Add("Text", _viewModel, nameof(_viewModel.ErrorMessage));
// 命令绑定到按钮
btnLogin.Click += (s, e) =>
{
if (_viewModel.LoginCommand.CanExecute(null))
_viewModel.LoginCommand.Execute(null);
};
// 监听命令可执行状态,同步按钮 Enabled
_viewModel.LoginCommand.CanExecuteChanged += (s, e) =>
btnLogin.Enabled = _viewModel.LoginCommand.CanExecute(null);
}
}
}

[NotifyCanExecuteChangedFor(nameof(LoginCommand))] 这个特性非常实用——当 Username 或 Password 属性变化时,它会自动触发 LoginCommand.CanExecuteChanged 事件,按钮的 Enabled 状态随即同步刷新,完全不需要手动调用 CommandManager.InvalidateRequerySuggested()。
Q:生成的属性代码在哪里看?
在 Visual Studio 中,展开"解决方案资源管理器 → 依赖项 → 分析器 → CommunityToolkit.Mvvm.SourceGenerators",可以直接查看所有生成的代码文件,方便调试和理解生成逻辑。
Q:partial 关键字忘了加会怎样?
编译器会直接报错,提示类必须声明为 partial。这是源生成器的硬性要求,因为生成的代码需要"插入"到同一个类中。
Q:能和现有的自定义 ViewModelBase 一起用吗?
可以,但需要让你的 ViewModelBase 继承自 ObservableObject,或者让 ViewModelBase 实现 INotifyPropertyChanged 并确保 ObservableObject 的方法签名兼容。最简单的做法是直接让 ViewModel 继承 ObservableObject。
Q:字段命名有什么规则?
源生成器识别以下命名风格:_fieldName、m_fieldName、fieldName,统一转换为首字母大写的公共属性名。推荐使用 _camelCase 风格,与 C# 社区惯例一致。
在一个包含 15 个 ViewModel、共 180 个绑定属性的真实 Winform 项目中(测试环境:.NET 8,VS 2022 17.9,CommunityToolkit.Mvvm 8.3.2),完成迁移后的数据如下:
在你的项目中,有没有遇到过因为 INotifyPropertyChanged 样板代码过多导致维护困难的情况?或者你目前用的是哪种方案来管理 ViewModel 的属性通知——是手写、T4 模板、还是已经在用源生成器了?欢迎在评论区聊聊你的实践经验。
另外一个值得思考的问题:源生成器这种"编译期代码生成"的思路,还有哪些场景可以用来消除项目中的样板代码? 比如序列化、依赖注入注册、枚举扩展方法……这个方向其实还有很多值得挖掘的地方。
[ObservableProperty]不是魔法,是编译器替你写了你本该写的代码。
源生成器的核心价值:把重复的、低价值的机械劳动交给工具,把精力留给真正的业务逻辑。
MVVM 的本质是关注点分离,而不是写更多的样板代码——工具选对了,架构才能真正落地。
如果你想深入这个方向,推荐按以下路径逐步展开:
[ObservableProperty] 与 [RelayCommand] 的核心用法[ObservableValidator] 实现数据验证集成(配合 DataAnnotations)相关信息
我用夸克网盘给你分享了「AppMvvm04.zip」,点击链接或复制整段内容,打开「夸克APP」即可获取。
/abc73YNjDq:/
链接:https://pan.quark.cn/s/fc3a901f37d2
提取码:YE8X
标签: C# WinForms MVVM CommunityToolkit.Mvvm 源生成器 INotifyPropertyChanged 性能优化 .NET
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!