Prism PropertyObserver 源码解析:链式属性订阅与动态重建

目标读者:熟悉 WPF/MVVM 基础,想深入理解 Prism ObservesProperty 链式表达式底层实现的中高级开发者。

阅读收获:理解 Prism 如何用不到 200 行代码实现"任意深度属性变化监听",并掌握其动态重建链路的内存安全设计。


一、它解决了什么问题

在 MVVM 中,我们经常需要让一个 CommandCanExecute 自动响应某个属性变化:

// 当 CurrentUser.Department.IsActive 变化时,刷新 SaveCommand 的可用性
SaveCommand.ObservesProperty(() => vm.CurrentUser.Department.IsActive);

这个 API 之所以能支持任意深度的属性链,底层依赖的就是 PropertyObserverPropertyObserverNode 这两个类。它的精妙之处在于:当中间层对象被替换时,能自动解绑旧对象、绑定新对象,避免内存泄漏


二、两个类的职责

角色 核心职责
PropertyObserver 入口 解析表达式树,拆解出属性链,构建链表
PropertyObserverNode 节点 监听一个属性变化,维护指向下一节点的引用

一句话总结:PropertyObserver 负责"拆链",PropertyObserverNode 负责"链上的每一环"。


三、表达式树解析:从 () => A.B.C 到链表

PropertyObserver 入口接收一个 Expression<Func<T>>,核心逻辑是用栈从右向左拆解:

Stack<PropertyInfo> stack = new Stack<PropertyInfo>();
for (;;)
{
    MemberExpression memberExpression = propertyExpression as MemberExpression;
    if (memberExpression == null) break;
    propertyExpression = memberExpression.Expression;
    stack.Push(memberExpression.Member as PropertyInfo);
}

对于表达式 () => ViewModel.User.Age,循环过程是:

轮次 识别出 剩余 栈状态(顶→底)
1 .Age ViewModel.User [Age]
2 .User ViewModel [Age, User]
3 ViewModelConstantExpression,终止 [Age, User, ViewModel]

随后用栈构建单向链表:

PropertyObserverNode node = new PropertyObserverNode(stack.Pop(), _action); // Age
PropertyObserverNode current = node;
foreach (var p in stack)
{
    var next = new PropertyObserverNode(p, _action);
    current.Next = next;
    current = next;
}

最终得到:[Age] → [User] → null,每个节点都持有同一个回调 _action


四、链式订阅与动态重建(核心设计)

4.1 订阅起点

SubscribeListenerFor 把根对象(这里是 ViewModel)的 PropertyChanged 挂上当前节点:

public void SubscribeListenerFor(INotifyPropertyChanged inpcObject)
{
    _inpcObject = inpcObject;
    _inpcObject.PropertyChanged += OnPropertyChanged;
    if (Next != null) GenerateNextNode();
}

4.2 向下递归订阅

GenerateNextNode 从当前节点持有的对象中取出下一层属性的值,作为下一节点的 INotifyPropertyChanged 源:

private void GenerateNextNode()
{
    object value = PropertyInfo.GetValue(_inpcObject);
    if (value == null) return;

    INotifyPropertyChanged inpc = value as INotifyPropertyChanged;
    if (inpc == null) throw new InvalidOperationException(...);

    Next.SubscribeListenerFor(inpc);
}

4.3 节点的 _action 委托:动态重建的钥匙

节点的构造函数里把 _action 包装成一个复合委托

_action = delegate()
{
    action?.Invoke();        // 1. 执行用户回调(刷新 Command)
    if (Next == null) return;
    Next.UnsubscribeListener();  // 2. 解绑旧子链
    GenerateNextNode();          // 3. 基于新对象重建子链
};

为什么要先解绑再重建?

User 属性被赋值为新对象时,旧 User 仍被事件订阅链强引用,无法被 GC。设计上以父级属性变化为信号,整条子链清掉、用新对象重建,这才是真正的"内存安全"。


五、事件触发与判定

private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e?.PropertyName == PropertyInfo.Name || string.IsNullOrEmpty(e?.PropertyName))
        _action?.Invoke();
}

注意 string.IsNullOrEmpty(e.PropertyName) 这个分支——部分类(如 BindingList)会发送"全局通知",这里靠它兜底。


六、完整流程图

表达式: () => ViewModel.User.Age
                ↓ 解析
        链表: [Age] → [User] → null
                ↓ 订阅
        节点[User] 订阅 ViewModel.PropertyChanged
        节点[Age]  订阅 User.PropertyChanged

场景 A: User 对象本身被替换为 NewUser
        → 节点[User] 触发
        → 回调:刷新 Command + 解绑 Age 节点 + 用 NewUser 重建子链

场景 B: User.Age 变化
        → 节点[Age] 触发
        → 回调:刷新 Command(不重建,因为没有 Next)

七、与 DelegateCommand 的联动

ObservesProperty 内部就是把 RaiseCanExecuteChanged 包装成一个 Action,交给 PropertyObserver

public DelegateCommand<T> ObservesProperty<TProperty>(Expression<Func<TProperty>> propertyExpression)
{
    var handler = new Action(RaiseCanExecuteChanged);
    _observer = PropertyObserver.Observes(propertyExpression, handler);
    return this;
}

RaiseCanExecuteChanged 最终调用 CommandManager.InvalidateRequerySuggested(),让 WPF 在下一个 UI 空闲周期重新查询所有 Command.CanExecute,按钮的 IsEnabled 自动更新。

整个链路:

属性变化 → PropertyObserverNode 触发 → RaiseCanExecuteChanged
        → CommandManager.InvalidateRequerySuggested()
        → WPF 重新查询 CanExecute → Button.IsEnabled 更新

八、值得借鉴的设计思想

亮点 启示
链式订阅 用链表把多层属性串联,每层只关心自己直接监听的属性
动态重建 中间层对象被替换时,整条子链解绑并重建
弱依赖 只要求实现 INotifyPropertyChanged,不依赖任何基类
内存安全 通过动态解绑避免对象替换导致的泄漏
通用性 监听深度不限制,表达式写多深就支持多深

架构师视角:当对象关系可能动态变化时,不要试图维护一张复杂的监听网——让变化发生时整条链路重建,代码会简洁得多。


九、实战示例

public class UserViewModel : BindableBase
{
    private string _name;
    public string Name { get => _name; set => SetProperty(ref _name, value); }

    private int _age;
    public int Age { get => _age; set => SetProperty(ref _age, value); }

    public DelegateCommand SaveCommand { get; }

    public UserViewModel()
    {
        SaveCommand = new DelegateCommand(Save, CanSave)
            .ObservesProperty(() => Name)
            .ObservesProperty(() => Age);
    }

    private bool CanSave() => !string.IsNullOrEmpty(Name) && Age > 0;
    private void Save() { /* 保存逻辑 */ }
}

输入 NameAge 时,按钮的可用状态完全自动刷新——开发者一行 PropertyChanged 订阅代码都不用写。

posted @ 2026-06-22 20:23  孤沉  阅读(7)  评论(0)    收藏  举报