65.9K
CodeProject 正在变化。 阅读更多。
Home

INotifyPropertyChanged 传播器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (12投票s)

2014年5月27日

CPOL

6分钟阅读

viewsIcon

42358

downloadIcon

94

免费且优雅的方式来传播依赖属性更改(包括动态可变嵌套属性)

注意

与此代码相比,我强烈建议使用 Stephen Cleary 的 Nito.CalculatedProperties 库: https://github.com/StephenCleary/CalculatedProperties。 

原来 Cleary 先生在我创建代码的同时开发并发布了他的库,所以当时我并不知道。 它比我的解决方案更加灵活和优雅,非常简单且有用,但却鲜为人知。

引言

以下是当依赖项发生更改时,传播依赖项属性更改(包括动态可更改的嵌套属性)的免费且优雅的方法。

背景

最流行和最天真的 INotifyPropertyChanged 实现看起来像这样:

        public decimal TotalPrice
        {
            get { return UnitPrice * Qty; }
        }
            public int Qty
        {
            get { return _qty; }
            set
            {
                _qty = value;
                RaisePropertyChanged(() => Qty);
                RaisePropertyChanged(() => TotalPrice);
            }
        }
        public decimal UnitPrice
        {
            get { return _unitPrice; }
            set
            {
                _unitPrice = value;
                RaisePropertyChanged(() => UnitPrice);
                RaisePropertyChanged(() => TotalPrice);
            }
        }

它有一个众所周知的设计缺陷:独立属性知道它们的依赖项(此示例中的 QtyUnitPrice 知道 TotalPrice 依赖于它们)。
对于小型应用程序来说这不算大问题,但在复杂的视图模型层次结构中,簿记代码会迅速增长,事情会很快失控。

难道没有类似 Microsoft Excel 表格的东西会更酷吗?你只需键入一个依赖于其他单元格的公式,而那些单元格却不知道有什么依赖于它们。

现有解决方案

社区已经认识到这个问题,并且已经存在一些可能的解决方案。我们为什么还需要另一个呢?好吧,让我们看看我们已经有什么

UpdateControls。一个非常有前途且有效的解决方案。然而,它要求放弃 INotifyPropertyChanged 以及传统的 {Binding} 扩展和转换器,这在遗留代码中并不容易。而且,一个人应该有一些勇气在新商业项目上尝试它。

Fody(Notify Property Weaver 的后继者)。在这里,我们进入了 IL 重写领域。Fody 是免费、快速且优秀的。不幸的是,它有一个很大的限制——即它只分析一个类范围内的依赖项,也就是说,如果它依赖于嵌套/子视图模型的属性,它将无法传播计算属性。

PostSharp。也是一个 IL 重写解决方案,它具有 Fody 的相同优点,并且能够完美处理嵌套依赖项。太好了!不幸的是,INotifyPropertyChanged 的实现是 PostSharp Ultimate 的一部分,而 PostSharp Ultimate 非常昂贵(截至 2014 年 5 月 27 日,价格为 589 欧元)。

这个列表显然不完整,还有其他可能的解决方案,有或没有类似的限制。然而,看起来只有 PostSharp 能够完全解决这个问题,不幸的是,它不是免费的。

介绍 PropertyChangedPropagator

长话短说,最好通过示例来解释如何使用它(请注意粗体语句)。

        public int Qty
        {
            get { return _qty; }
            set { Set(ref _qty, value); }
        }

        public decimal UnitPrice
        {
            get { return _unitPrice; }
            set { Set(ref _unitPrice, value); }
        }

        public decimal TotalPrice
        {
            get
            {
                RaiseMeWhen(this, has => has.Changed(_ => _.UnitPrice, _ => _.Qty));
                return UnitPrice * Qty;
            }
        }

        public decimal ExchTotalPrice
        {
            get
            {
                RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice, _ => _.ExchangeRate));
                RaiseMeWhenDynamic(ExchangeRate, has => has.Changed(_ => _.Rate));
                return TotalPrice * ExchangeRate.Rate;
            }
        }

        public ExchangeRateViewModel ExchangeRate
        {
            get { return _exchangeRate; }
            set { Set(ref _exchangeRate, value); }
        }  

如您所见,初始示例被颠倒过来了——QtyUnitPrice 属性(以及嵌套视图模型的 ExchangeRate.Rate 属性)不知道是否有任何东西依赖于它们,它们只是负责触发自己的通知。而计算属性明确说明“是的,我依赖于这个和那个,请在其中任何一个发生变化时通知我”。

RaiseMeWhen 如何知道要传播哪个属性?它使用 [CallerMemberName] 属性。

public void RaiseMeWhen<T>(T dep, Action<IRaiseMeWhenHas<T>> has, 
    [CallerMemberName] string propertyName = null) ...   

虽然我的解决方案确实违反了“不要重复自己”的原则(以及初始的朴素示例),但它鼓励在依赖项所属的地方——即依赖属性的 getter 中——声明依赖项。

高级细节

这里最棘手的部分是 RaiseMeWhenDynamic。如果某个属性依赖于嵌套视图模型的属性,并且该嵌套视图模型本身在运行时会发生变化,则应使用它。听起来很复杂,但实际上很简单。在附加示例中,我们可以选择一个 ExchangeRateViewModel 嵌套视图模型,并更改当前活动对象的 Rate 属性。

动态依赖项的一个缺点是,如果存在多个相同类型的嵌套依赖项,则需要添加一些 string 提示来区分一个动态依赖项与其他依赖项(幸运的是,这种情况很少见)。考虑伪代码:

        public string CurrentContactName
        {
            get
            {
        // PrimaryContact and SecondaryContact are of same type so have to use hints
                RaiseMeWhenDynamic(PrimaryContact, 
                "Primary", has => has.Changed(_ => _.Name));
                RaiseMeWhenDynamic(SecondaryContact, 
                "Secondary", has => has.Changed(_ => _.Name));
                return (PrimaryContact ?? SecondaryContact).Name;
            }
        } 

INotifyPropertyChanged 一个未被充分认识的特性是,可以触发带有 string.Empty 属性名称的 PropertyChanged 事件,这意味着“嘿,整个对象都改变了,刷新所有属性”。如果从构造函数调用 RaiseMeWhen,它就可以做到这一点(注意:它不适用于动态依赖项)。另一种激进的方法是在嵌套视图模型的任何属性发生更改时触发通知。下面的代码演示了这两种功能。

public MyViewModel(NestedViewModel nested)
{
    NestedVm = nested;
    RaiseMeWhen(NestedVm, has => has.ChangedAny());
} 

PropertyChangePropagator 的使用可能看起来像声明式代码,但没有 IL 重写,并且实现是完全命令式的。这意味着所有传播订阅都是惰性创建的(或者在动态订阅的情况下动态修改),只有在调用依赖属性的 getter 时才会触发。用通俗的话说——如果没人读取计算属性,那么它就不会被传播。这种“特性般的 bug”在现实生活中通常不是问题(毕竟,您为视图声明属性是为了让它们读取),但它使单元测试有点棘手。:-) 请查看源代码了解更多详情。

实现也是线程安全的,并使用弱事件侦听器,因此可以安全地声明对长寿命发布者的依赖关系,因为订阅者将在超出范围时符合垃圾回收条件。还有一个实现技巧可以优化内存使用。

我强烈建议下载并运行源代码以了解更多详细信息。

性能

该解决方案包括性能测试,其结果比朴素实现慢 1.5-2 倍。PostSharp 中的类似方案在我机器上慢 9 倍,但我没有将其包含在附加代码中,因为存在许可依赖。总的来说,性能将取决于您的视图模型层次结构的实际复杂性。

订阅回调在第一次 getter 调用时惰性创建,然后缓存。在动态依赖项的情况下,在引用动态子视图模型更改后的后续读取时,订阅将重新创建。

用于指定依赖项的 Lambda 表达式树本身是“has”回调体的一部分,因此仅在订阅期间构建一次。

RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice, _ => _.ExchangeRate));

传播处理程序被设为 static 以优化内存占用,即,为相同类型的多个视图模型使用相同的生成方法实例。有关实现细节,请参阅 StaticPropertyChangedHandlersCache 类。

如何将其添加到我的项目中?

很简单!复制 PropertyChangedPropagator.csWeakEventListener.cs,并查看 BindableBaseEx.cs 以了解如何将 RaiseMeWhen* 方法包含在您的基视图模型类中。

它在 WPF 和 WinRT 中同样适用(没有尝试过 Silverlight,但应该也不错)。

可能的改进

最好添加对 ObservableCollection<T> 的支持,并找到一种方法来处理多个相同类型的动态依赖项,而无需 string 提示。

摘要

PropertyChangedPropagator 可以作为一个免费且简单的解决方案来传播属性依赖项。它能很好地与遗留代码配合使用,并可以逐步采用。但是,如果您已经拥有 PostSharp Ultimate 许可证(或有空闲的钱),那么我建议您使用它,因为它是一个更成熟的解决方案,并提供官方支持。

历史

  • 2014 年 6 月 5 日;添加了性能部分。
  • 2014 年 5 月 27 日:初始版本
© . All rights reserved.