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

AutoPropertyChanged - 类型安全的 INotifyPropertyChanged 实现

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.54/5 (11投票s)

2010年5月24日

CPOL

6分钟阅读

viewsIcon

36752

downloadIcon

244

无需运行时反射和 Lambda 表达式即可实现类型安全的 INotifyPropertyChanged 实现。

引言

我猜任何深入研究 WPF 的人都知道 ObservableCollection 类如何能无缝集成到 WPF 的数据绑定中。

问题是,一旦你开始做一些简单的事情,比如将 ViewModel 的属性链接到 CheckBox 控件,你不可避免地会开始想,是否真的需要每次都手动实现 INotificationPropertyChanged 接口。

对我来说,直接实现此接口的最大缺点之一是需要将属性名称作为字符串重复,因为 PropertyChangedEventArgs 参数需要属性名称。一旦你开始重构,最好不要忘记更新所有引用该属性的地方 - 如果你漏掉一个地方,你将在运行时付出代价 :-)

另一个缺点是,每次向 ViewModel 添加新类时,都需要编写大量的代码。此外,通常每次任何属性更改时,你都会构造一个 "new PropertyChangedEventArgs" 对象。这会影响托管堆并降低性能。

现有解决方案的精彩总结可以在 此处找到(顺便说一句,这也是一个很棒的网站)。

据我所知,本文试图以一种新颖的方式解决这个烦恼。使用 AutoPropertyChanged,您可以绕过上述大多数问题。

演示项目和Utility.AutoPropertyChangedNotification项目都是使用 Visual Studio 2010 和 .NET 4.0 创建的。这篇博文描述了如何在 2008 版本中打开 2010 解决方案和项目文件 - 我自己从未尝试过。

Using the Code

首先,想要公开其属性的类需要派生自 ImplementsPropertyChanged类。这可能是一个很大的缺点,但另一方面,您可以自由地将属性绑定到 ViewModel 的属性,而不是直接绑定到 ViewModel,因此您可以将 ViewModel 保持原样,并包含 ImplementsPropertyChanged 对象而不是直接派生。

第一步如下所示

public class ListItem : ImplementsPropertyChanged
{
    public ListItem()
    {
    }
}

好的,这几乎什么也没做。ImplementsPropertyChanged 向类添加了一个 PropertyChangedEventHandler 事件,并提供了 OnPropertyChanged的默认实现。

让我们向类添加一个属性

public class ListItem : ImplementsPropertyChanged
{
    public ListItem()
    {
    }

    [AutoPropertyChangedNotification]
    public AutoPropertyChanged<ListItem, bool, Property0> IsChecked
    {
        get;
        set;
    }
}

这段代码现在还无法运行。您需要初始化 IsChecked才能使其正常工作

public class ListItem : ImplementsPropertyChanged
{
    public ListItem()
    {
        IsChecked = Prop.New(IsChecked, this);
    }

    [AutoPropertyChangedNotification]
    public AutoPropertyChanged<ListItem, bool, Property0> IsChecked
    {
        get;
        set;
    }
}

完成 - 就这些(除了在启动时调用 AutoPropertyInitializer.InitializeProperties())。

AutoPropertyChanged 有自己的属性 Value。因此,从代码或 XAML 访问属性将如下所示

ListItem myItem = new ListItem();
myItem.IsChecked.Value = true;

您在使用它时需要记住这一点 - 我没有找到绕过它的方法,因为索引器至少需要一个参数。

XAML 中的代码隐藏可能如下所示

<CheckBox Content="Check this box" Name="checkBox1" 
          IsChecked="{Binding Path=IsChecked.Value}" />

但这个泛型参数 Property0又是什么呢?

这是此解决方案不太理想的方面。如上所述,问题在于如何告诉 .NET 运行时将静态 PropertyChangedEventArgs 字段区分开,我没有找到更好的方法。

但它仍然有效,如果你搞砸了(例如,为多个属性使用了相同的 PropertyN 字段),你将在一次性初始化阶段通过捕获 DuplicatePropertyFoundException 来学习这一点 :-)。

它是如何工作的?

AutoPropertyChangedNotification

首先,显然有这个属性 AutoPropertyChangedNotification。它的实现没有任何花哨的东西,只是一个简单而朴素的属性,只能设置在属性上

[AttributeUsage(AttributeTargets.Property)]
public sealed class AutoPropertyChangedNotificationAttribute : Attribute
{
}

它的目的是让 AutoPropertyChanged属性(通过反射)易于识别 - 也稍微向读者表明后台有一些额外的事情在发生。

Prop.New

静态方法 Prop.New 只是一个简化 - 通常,您需要像这样创建属性

IsChecked = new AutoPropertyChanged<ListItem, bool, Property0>(this);

这看起来容易出错。Prop.New缓解了这个问题

public static class Prop
{
    public static AutoPropertyChanged<T0, T1, T2> New<T0, T1, T2>(
           AutoPropertyChanged<T0, T1, T2> field, T0 hostClass) 
           where T0 : ImplementsPropertyChanged
    {
        return new AutoPropertyChanged<T0, T1, T2>(hostClass);
    }
}

可能,这看起来不好。肯定不好,但它编译成相当多的 IL 指令,并且没有运行时开销 - 并使生成的代码更易读。

AutoPropertyChanged

泛型类 AutoPropertyChanged涉及的内容更多一些

public class AutoPropertyChanged<TBase, TProperty, 
       TPropertyName> where TBase : ImplementsPropertyChanged
{
    public static PropertyChangedEventArgs propertyChanged;
    public AutoPropertyChanged(TBase parent)
    {
        _parent = parent;
    }
    private TBase _parent;
    private TProperty _value;
    public TProperty Value
    {
        get
        {
            return _value;
        }
        set
        {
            _value = value;
            _parent.OnPropertyChanged(propertyChanged);
        }
    }
}

这里有前面提到的静态 PropertyChangedEventArgs 字段。如您所见,这里仍然没有什么特别之处:两个动态字段 (_parent _value),其余都是简单的根据需要调用事件。

显然,这会引入一点开销 - _parent 引用对于调用 PropertyChanged 事件是必需的;如果我们手动实现 IPropertyChangedNotification 接口,则不需要这个。

这是运行时需要执行的所有内容。直到这里都没有反射和 lambda 表达式 - 反射只在一次性初始化阶段需要,并且发生在那里。

AutoPropertyInitializer.InitializeProperties

这个静态方法触发属性的一次性初始化。大多数丑陋的代码都包含在那里。我们来看看

// ... Iterating all loaded assemblies,
// enumerating their types omitted for brevity. Rest:
// Set AutoPropertyAttribute static PropertyChangedEventArgs fields
PropertyInfo[] props = type.GetProperties();
foreach (PropertyInfo prop in props)
{
    if (prop.GetCustomAttributes(typeof(
        AutoPropertyChangedNotificationAttribute), false).Length > 0)
    {
        if (!prop.PropertyType.Name.StartsWith("AutoPropertyChanged"))
        {
            throw new AutoPropertyChangedNotificationAttributeIncorrectlyUsedException(
              BuildExceptionMessage(type, prop, "The attribute " + 
              "[AutoPropertyChangedNotification] may only be used " + 
              "if the following property is an AutoPropertyChanged property.") );
        }
        if( prop.PropertyType.GetField("propertyChanged").GetValue(null)!=null )
        {
            throw new DuplicatePropertyFoundException(
              BuildExceptionMessage(type, prop, "AutoPropertyChanged properties " + 
              "must be distinguished within one class by the last template " + 
              "parameter (Property0...Property19)."));
        }
        prop.PropertyType.GetField("propertyChanged").SetValue(null, 
             new System.ComponentModel.PropertyChangedEventArgs(prop.Name));
    }
}

最有趣的一行 - 除了异常部分 - 是 SetValue那一行。那就是初始化 AutoPropertyChanged 的静态字段的地方。仅此而已。

此行之上的两个检查指出了典型的错误场景。

如果你将一个类的多个属性设置为相同的 PropertyN泛型参数,你将收到一个 DuplicatePropertyFoundException。异常消息将描述哪个类和哪个属性是原因,因此修复它应该不难。

另一种情况是,如果属性 [AutoPropertyChangedNotification]设置在一个不是 AutoPropertyChanged类型的属性上。在这种情况下,会抛出 AutoPropertyChangedNotificationAttributeIncorrectlyUsedException (感谢上帝,有自动完成)。您还将收到一个足够详细的消息,可以直接指向有问题的属性。

演示应用程序

演示应用程序在很大程度上受到了这篇文章的启发(谢谢,Philip)- 顺便说一句,这篇文章拥有我遇到的 WPF 中最好的 TreeView 实现之一。通过仔细研究这个项目,我学到了很多关于 WPF 的知识 :-)

看起来是这样的:

Screenshot of Demo application

左侧是一些控件,它们与右侧的控件进行交互(反之亦然)。

应用程序本身使用 MVVM 视图模型,有一个 ViewModel(DemoViewModel)和两个 View(LeftView RightView)。

由于它包含的代码量不多,我认为最好您自己探索源代码。

摘要

优点

  • 运行时开销低(当然仍然比手动编码多)
    • 每个属性一个额外的字段,以及
    • 每个属性一个指向其父级的引用
  • 没有动态创建 PropertyChangedEventArgs
  • 类型安全,因此重构安全
  • 相当容易使用

缺点

  • 需要 Property0...PropertyN 来区分不同的属性(但对此没有运行时开销)
  • 需要公开属性的类派生自一个基类(我非常确定您可以摆脱基类,我正在研究这个问题)

更改日志

  • 1.0:首次公开版本(2010-05-24)
© . All rights reserved.