直接用INotifyPropertyChanged易出错,主因是字符串硬编码属性名导致拼写错误、重命名不同步等问题;CommunityToolkit.Mvvm通过源生成器在编译期自动生成通知逻辑,杜绝硬编码、支持IDE重命名联动、零运行时开销。

为什么直接用 INotifyPropertyChanged 写 MVVM 很容易出错
手写 INotifyPropertyChanged 实现,90% 的问题出在字符串硬编码的属性名上——比如 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("UserName")),拼错、重命名没同步、大小写不一致,运行时完全不报错,只导致 UI 不更新。更麻烦的是,每次改属性还得手动加 OnPropertyChanged 调用,逻辑分散、易漏、难维护。
CommunityToolkit.Mvvm 就是为干掉这些重复劳动而生的:它用源生成器(source generator)在编译期自动注入通知逻辑,不依赖反射,零运行时开销,还能和 IDE 重命名联动。
- 必须用 C# 9+ 和 .NET 5+(源生成器依赖)
- 不能在
dynamic或匿名类型上用[ObservableProperty] -
set访问器里别写耗时操作——通知触发是同步的,卡 UI
[ObservableProperty] 怎么让一个字段自动通知变更
这是最常用也最容易上手的特性。给字段加 [ObservableProperty],工具包会自动生成对应属性、On<propertyname>Changed</propertyname> 方法、以及符合 INPC 规范的 setter。
using CommunityToolkit.Mvvm.ComponentModel;
<p>public partial class UserViewModel : ObservableObject
{
[ObservableProperty]
private string? _name;</p><pre class="brush:php;toolbar:false;">[ObservableProperty]
private int _age;}
编译后等效于你手动写了 Name 和 Age 属性,每个 setter 都调用 OnPropertyChanged,且属性名由编译器推导,绝不会拼错。
- 字段名必须以下划线开头(如
_name),否则生成失败 - 支持
readonly字段(生成只读属性),但不支持init(目前源生成器不处理 init-only set) - 如果需要自定义 setter 逻辑(比如校验),用
[NotifyPropertyChangedFor]配合手动属性更稳妥
怎么在属性变更时执行额外逻辑(比如验证或联动)
[ObservableProperty] 本身不支持拦截 setter,但你可以用 On<propertyname>Changed</propertyname> 钩子方法——这是源生成器为你预留的扩展点,命名必须严格匹配(首字母大写 + “Changed” 后缀)。
public partial class UserViewModel : ObservableObject
{
[ObservableProperty]
private string? _email;
<pre class="brush:php;toolbar:false;">partial void OnEmailChanged(string? value)
{
if (!string.IsNullOrWhiteSpace(value) && !value.Contains('@'))
{
// 这里可以抛异常、设错误状态、或触发另一个属性更新
EmailError = "邮箱格式不正确";
}
else
{
EmailError = null;
}
}
[ObservableProperty]
private string? _emailError;}
注意:OnEmailChanged 是 partial 方法,必须声明在同一个 partial class 里,且签名要完全一致(参数类型、顺序、是否可空);否则编译器直接忽略,不报错也不调用。
- 不要在
OnXxxChanged里再赋值给同名字段(比如又写_email = value),会引发无限循环 - 如果需要异步响应(比如保存到数据库),请启动新任务,别阻塞这个回调
- 多个字段联动时,优先用
[NotifyCanExecuteChangedFor]或RelayCommand的CanExecute依赖项,而不是在钩子里手动调OnPropertyChanged
RelayCommand 和 AsyncRelayCommand 怎么避免命令状态不同步
手写 ICommand 最常踩的坑是 CanExecute 不刷新:比如按钮绑定了 DeleteCommand,但删除条件变了(如选中项为空),按钮却还是可点。CommunityToolkit 提供的命令类型默认支持自动重评估,前提是你要显式声明依赖项。
public partial class ItemListViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<Item> _items;
<pre class="brush:php;toolbar:false;">[ObservableProperty]
private Item? _selectedItem;
public RelayCommand DeleteCommand { get; }
public ItemListViewModel()
{
DeleteCommand = new RelayCommand(
ExecuteDelete,
() => SelectedItem != null
);
// 告诉命令:当 SelectedItem 变化时,重新调用 CanExecute
DeleteCommand.NotifyCanExecuteChangedFor(nameof(SelectedItem));
}
private void ExecuteDelete() => Items.Remove(SelectedItem);}
关键点是 NotifyCanExecuteChangedFor —— 它不是魔法,只是把属性名注册进命令的监听列表,底层靠 INotifyPropertyChanged 事件触发重评估。
- 如果依赖多个属性(比如
SelectedItem != null && IsEditing),就多次调用NotifyCanExecuteChangedFor -
AsyncRelayCommand默认禁用命令(执行中变灰),但不会自动恢复;需手动调CommandManager.InvalidateRequerySuggested()或用NotifyCanExecuteChangedFor关联状态字段 - WPF 中若用
CommandManager.RequerySuggested全局刷新,性能差且不可控,应避免
真正麻烦的从来不是“怎么写”,而是“什么时候不该用”——比如高频更新的传感器数据、动画帧绑定、或嵌套很深的集合结构,[ObservableProperty] 生成的细粒度通知反而造成大量 UI 刷新。这时候该切回手动 OnPropertyChanged 批量通知,或者换用 ObservableCollection<t></t> 的 Reset 操作。工具越省事,越得清楚它替你屏蔽了什么。


















