MVVM Toolkit 源代码生成器笔记:[ObservableProperty] 与 [NotifyPropertyChangedFor]
一、一句话概括(秒懂版)
[ObservableProperty]:告诉编译器——“给我这个私有字段自动生成一个公共属性,并且这个属性的 setter 会自动触发 UI 刷新通知”。[NotifyPropertyChangedFor(nameof(FullName))]:告诉编译器——“当这个字段变化时,顺便也通知 UI 说FullName这个属性也变了”。
二、你在看什么代码?(上下文)
这是 CommunityToolkit.MVVM(微软官方 MVVM 工具包)中的源代码生成器(Source Generator) 功能。它利用 C# 编译器在编译时“帮我们写重复的样板代码”。
public partial class MyViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _lastName;
// 这是一个只读的计算属性(依赖上面两个值)
public string FullName => $"{FirstName} {LastName}";
}
关键点:类必须是 partial(分部类),因为生成器会往同一个类的另一个文件中“追加代码”。
三、逐特性拆解(深入底层)
1. 第一个特性:[ObservableProperty]
作用对象:私有字段(_firstName, _lastName)
编译时行为:生成器会找到这个字段,生成一个同名的公共属性(去掉下划线,首字母大写)。
生成前(你写的):
[ObservableProperty]
private string _firstName;
生成后(编译器实际编译的,伪代码):
private string _firstName;
public string FirstName
{
get => _firstName;
set
{
// 判断值是否真的变了,避免无限循环
if (!EqualityComparer<string>.Default.Equals(_firstName, value))
{
_firstName = value;
// 🟢 核心:自动触发 UI 刷新通知
OnPropertyChanged(nameof(FirstName));
}
}
}
如果没有这个特性,你每次都要手动写上面的 get/set 和 OnPropertyChanged,非常繁琐。
2. 第二个特性:[NotifyPropertyChangedFor(nameof(FullName))]
作用对象:依然放在字段上,但它是补充通知。
含义:在刚才生成的 set 方法中,额外加一行通知代码。
生成后(完整伪代码):
private string _firstName;
public string FirstName
{
get => _firstName;
set
{
if (!EqualityComparer<string>.Default.Equals(_firstName, value))
{
_firstName = value;
// 1. 通知自己的变化(由 [ObservableProperty] 提供)
OnPropertyChanged(nameof(FirstName));
// 2. 🟢 额外通知:FullName 也变了(由 [NotifyPropertyChangedFor] 提供)
OnPropertyChanged(nameof(FullName));
}
}
}
同理,_lastName 的 setter 也会自动加上通知 FullName 变化的逻辑。
四、为什么需要这样写?(痛点场景)
假设没有这两个特性,纯手动写:
public partial class MyViewModel : ObservableObject
{
private string _firstName;
private string _lastName;
public string FirstName
{
get => _firstName;
set
{
if (SetProperty(ref _firstName, value)) // SetProperty 是基类方法
{
// ⚠️ 手动维护依赖关系!
OnPropertyChanged(nameof(FullName));
}
}
}
public string LastName
{
get => _lastName;
set
{
if (SetProperty(ref _lastName, value))
{
// ⚠️ 又要手动写一遍!
OnPropertyChanged(nameof(FullName));
}
}
}
public string FullName => $"{FirstName} {LastName}";
}
痛点:
- 代码冗长(每次都要写 get/set)。
- 容易遗漏(如果 FullName 依赖 5 个字段,你要在 5 个 setter 里写通知,漏一个就出 Bug)。
- 维护困难(如果 FullName 的依赖变了,你要改所有 setter)。
有了特性后:只需在字段上贴标签,编译器帮你把上述所有模板代码“自动填充”进编译结果。
五、图解:运行流程(WPF / 数据绑定)
当用户在 UI 的文本框输入内容时:
用户在 TextBox 中输入 "John"
│
▼
绑定系统调用 set_FirstName("John")
│
▼
┌─────────────────────────────────────────┐
│ setter 内部(由生成器生成): │
│ 1. 赋值 _firstName = "John" │
│ 2. OnPropertyChanged("FirstName") → 通知 UI 刷新这个字段 │
│ 3. OnPropertyChanged("FullName") → 通知 UI 刷新全名 │
└─────────────────────────────────────────┘
│
▼
UI 自动更新:
- FirstName 对应的 Label 变为 "John"
- FullName 对应的 Label 变为 "John Smith"
关键:如果没有 [NotifyPropertyChangedFor],虽然 FirstName 变了,但 FullName 的绑定不会刷新,UI 上会显示旧值("null Smith")。
六、为什么用 nameof(FullName) 而不是字符串 "FullName"?
| 写法 | 优点 | 缺点 |
|---|---|---|
"FullName" |
简单直接 | 如果重构改名(如改为 FullNameText),编译器不报错,但运行时绑定失效,极难排查 |
nameof(FullName) |
编译时检查,重构自动跟随 | 无(强烈推荐) |
所以看到 nameof 要感到亲切——这是 C# 中最安全的写法。
七、重要注意事项(避坑指南)
-
类必须声明为
partial
因为生成器是往另一个文件里塞代码,如果不加partial,编译会报错。 -
字段命名约定:
通常用_camelCase(如_firstName),生成器默认会生成PascalCase的属性(FirstName)。尽量不要打破这个约定,否则生成的属性名可能不符合预期。 -
不要在字段上同时手动写属性:
如果你写了[ObservableProperty] private string _firstName;,又手动写了public string FirstName { get; set; },会产生冲突(重复定义)。二选一。 -
避免循环依赖通知:
如果 A 变化时通知 B 变化,B 变化时又通知 A 变化,会导致无限递归。虽然生成器会做一定防护(SetProperty有值变更判断),但设计时要避免逻辑死循环。
八、总结(便于记忆的口诀)
[ObservableProperty]:字段变属性,Setter 自动带通知。
[NotifyPropertyChangedFor]:一龙牵动全身,依赖属性跟着刷新。
二者合用,UI 绑定稳如泰山,代码量减少一半!
九、思考题(自检用)
- 如果去掉
[NotifyPropertyChangedFor(nameof(FullName))],在 UI 上修改FirstName后,FullName的显示会更新吗?为什么? - 假如
FullName的 getter 改为$"{_firstName} {_lastName}"(直接访问字段),和不改有区别吗?(提示:属性通知机制是否介入) - 你能想象编译器生成的完整
LastName属性 setter 长什么样吗?
学习感悟:刚开始看这堆特性觉得像“黑魔法”,但理解到“编译器只是帮我偷懒写重复代码”之后,就豁然开朗了。MVVM Toolkit 让 ViewModel 的编写从“体力活”变成了“声明式配置”,让我们能专注于业务逻辑,而不是机械地敲 get/set。
希望这篇笔记能帮你彻底拿下这个知识点!如果有疑问,随时来问。🚀

浙公网安备 33010602011771号