AutoPropertyChanged - 类型安全的 INotifyPropertyChanged 实现
无需运行时反射和 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 的知识 :-)
看起来是这样的:
左侧是一些控件,它们与右侧的控件进行交互(反之亦然)。
应用程序本身使用 MVVM 视图模型,有一个 ViewModel(DemoViewModel
)和两个 View(LeftView
和 RightView
)。
由于它包含的代码量不多,我认为最好您自己探索源代码。
摘要
优点
- 运行时开销低(当然仍然比手动编码多)
- 每个属性一个额外的字段,以及
- 每个属性一个指向其父级的引用
- 没有动态创建
PropertyChangedEventArgs
- 类型安全,因此重构安全
- 相当容易使用
缺点
- 需要 Property0...PropertyN 来区分不同的属性(但对此没有运行时开销)
- 需要公开属性的类派生自一个基类(我非常确定您可以摆脱基类,我正在研究这个问题)
更改日志
- 1.0:首次公开版本(2010-05-24)