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# 中最安全的写法。

七、重要注意事项(避坑指南)

  1. 类必须声明为 partial
    因为生成器是往另一个文件里塞代码,如果不加 partial,编译会报错。

  2. 字段命名约定:
    通常用 _camelCase(如 _firstName),生成器默认会生成 PascalCase 的属性(FirstName)。尽量不要打破这个约定,否则生成的属性名可能不符合预期。

  3. 不要在字段上同时手动写属性:
    如果你写了 [ObservableProperty] private string _firstName;,又手动写了 public string FirstName { get; set; },会产生冲突(重复定义)。二选一。

  4. 避免循环依赖通知:
    如果 A 变化时通知 B 变化,B 变化时又通知 A 变化,会导致无限递归。虽然生成器会做一定防护(SetProperty 有值变更判断),但设计时要避免逻辑死循环。

八、总结(便于记忆的口诀)

[ObservableProperty]:字段变属性,Setter 自动带通知。
[NotifyPropertyChangedFor]:一龙牵动全身,依赖属性跟着刷新。
二者合用,UI 绑定稳如泰山,代码量减少一半!

九、思考题(自检用)

  1. 如果去掉 [NotifyPropertyChangedFor(nameof(FullName))],在 UI 上修改 FirstName 后,FullName 的显示会更新吗?为什么?
  2. 假如 FullName 的 getter 改为 $"{_firstName} {_lastName}"(直接访问字段),和不改有区别吗?(提示:属性通知机制是否介入)
  3. 你能想象编译器生成的完整 LastName 属性 setter 长什么样吗?

学习感悟:刚开始看这堆特性觉得像“黑魔法”,但理解到“编译器只是帮我偷懒写重复代码”之后,就豁然开朗了。MVVM Toolkit 让 ViewModel 的编写从“体力活”变成了“声明式配置”,让我们能专注于业务逻辑,而不是机械地敲 get/set。

希望这篇笔记能帮你彻底拿下这个知识点!如果有疑问,随时来问。🚀

posted @ 2026-07-10 09:05  春煦景明  阅读(76)  评论(0)    收藏  举报