INotifyPropertyChanged - 自动实现( 重装)
使用代理生成器实现 INotifyPropertyChanged 和模型中的更改验证
引言
本文介绍如何通过一个新的接口 ICanBeDirty 自动实现 INotifyPropertyChanged 接口,该接口使用自定义生成的代理来检测模型是否发生更改。通常,INotifyPropertyChanged 和更改检测在 WPF 绑定场景中非常有用。Simon Cropp 有一篇 文章 描述了许多可能的解决方案;Simon 本人提出了一个在编译时将 INotifyPropertyChanged 代码注入属性的解决方案。
此解决方案基于 Einar Ingebrigtsen 的出色工作,他提出了一个 ProxyGenerator
来转换这个...
public class Foo
{
public virtual string Name1 { get; set; }
public string Name2
{
get
{
return Name1 + Date1.ToString();
}
}
public virtual bool Boolean1 { get; set; }
public virtual DateTime Date1 { get; set; }
public virtual object Value1 { get; set; }
}
...变成这样...
public class FooDerived : Foo, INotifyPropertyChanged, ICanBeDirty
{
private IDispatcher _dispatcher = DispatcherManager.Current;
public FooDerived(Foo source)
{
Date1 = source.Date1;
Name1 = source.Name1;
Boolean1 = source.Boolean1;
Value1 = source.Value1;
}
public override string Name1
{
get { return base.Name1; }
set
{
if (!Equals(base.Name1, value))
{
base.Name1 = value;
OnPropertyChanged("Name1");
OnPropertyChanged("Name2");
}
}
}
public override object Value1
{
get { return base.Value1; }
set
{
if (!Equals(base.Value1, value))
{
base.Value1 = value;
OnPropertyChanged("Value1");
}
}
}
public override bool Boolean1
{
get { return base.Boolean1; }
set
{
if (base.Boolean1 != value)
{
base.Boolean1 = value;
OnPropertyChanged("Boolean1");
}
}
}
public override DateTime Date1
{
get { return base.Date1; }
set
{
if (base.Date1 != value)
{
base.Date1 = value;
OnPropertyChanged("Date1");
OnPropertyChanged("Name2");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
SetDirty();
OnPropertyChangedInner(name);
}
protected void OnPropertyChangedInner(string name)
{
//Given a multithreading environment, if PropertyChanged is null,
//we leave it and an Exception will be thrown
//It is up to the invoker to control the event subscriptions
if (null != PropertyChanged)
{
if (_dispatcher.CheckAccess())
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
else
{
_dispatcher.BeginInvoke
(PropertyChanged, this, new PropertyChangedEventArgs(name));
}
}
}
private bool isDirty;
public bool IsDirty
{
get
{
return isDirty;
}
}
public void Save()
{
if (isDirty)
{
isDirty = false;
OnPropertyChangedInner("IsDirty");
}
}
private void SetDirty()
{
if (!isDirty)
{
isDirty = true;
OnPropertyChangedInner("IsDirty");
}
}
}
为什么扩展 Einar 的解决方案?主要原因是,在 WPF 绑定场景中,开发人员通常需要某种形式的更改验证;此解决方案通过实现 ICanBeDirty 接口,扩展了之前的解决方案,增加了“脏数据检查”,并添加了复制构造函数和相关属性的自动 PropertyChanged
事件,这意味着开发人员无需手动设置属性中的依赖关系。此解决方案有一个优化,可以在引发事件之前检查新值是否与旧值不同。此解决方案专为 WPF 绑定设计,因此实例化代理的性能损失并不重要,在“Points of Interest”部分将展示一些性能测试结果。
Using the Code
此解决方案允许开发人员编写如下代码
private static void ProxyWeaverTest()
{
//Setting up the DispatcherManager
DispatcherManager.Current = new Dispatcher
(System.Windows.Threading.Dispatcher.CurrentDispatcher);
//Getting the proxy
var weaver = new NotifyingObjectWeaver();
var myFoo = new Foo()
{
Boolean1 = true,
Value1 = new { LittleFoo = 2 },
Date1 = DateTime.Now,
Name1 = "Name1"
};
//Create a proxy copying the base instance
Foo myFooCopy = weaver.CreateInstance(myFoo);
//Getting the ICanBeDirty interface
var fooCanBeDirty = myFooCopy as ICanBeDirty;
System.Console.WriteLine(string.Format
("myFooCopy.Boolean1 = {0}", myFooCopy.Boolean1));
System.Console.WriteLine(string.Format
("myFooCopy.Value1 = {0}", myFooCopy.Value1));
System.Console.WriteLine(string.Format
("myFooCopy.Name1 = {0}", myFooCopy.Name1));
System.Console.WriteLine(string.Format
("myFooCopy.Name2 = {0}", myFooCopy.Name2));
System.Console.WriteLine(string.Format
("myFooCopy.Date1 = {0}", myFooCopy.Date1));
System.Console.WriteLine(string.Format
("myFooCopy.IsDirty after copy = {0}", fooCanBeDirty.IsDirty));
System.Console.WriteLine("Subscribing to PropertyChanged event ...");
(myFooCopy as INotifyPropertyChanged).PropertyChanged +=
(s, e) => System.Console.WriteLine(string.Format
("Changed :{0}", e.PropertyName));
System.Console.WriteLine(string.Format
("Assigning {0} to {1} ...", "myFooCopy.Name1", "New Name1"));
//Changing Name1 value must raise changes for IsDirty, Name1 and Name2
myFooCopy.Name1 = "New Name1";
System.Console.WriteLine(string.Format
("myFooCopy.IsDirty after changes = {0}", fooCanBeDirty.IsDirty));
//Changing Date1 value must raise changes for Date1 and Name2,
//IsDirty does not change
var newDate = DateTime.Now.AddDays(1);
System.Console.WriteLine(string.Format
("Assigning {0} to {1} ...", "myFooCopy.Date1", newDate));
myFooCopy.Date1 = newDate;
//Save the copy to reset the IsDirty flag and raise change event for IsDirty
System.Console.WriteLine("Saving myFooCopy ...");
fooCanBeDirty.Save();
System.Console.WriteLine(string.Format
("myFooCopy.IsDirty after saving myFooCopy = {0}", fooCanBeDirty.IsDirty));
System.Console.WriteLine("Press any key to exit ...");
System.Console.ReadKey();
}
注意,首先要设置 DispatcherManager
以使代理“Dispatcher
”友好,有关更多详细信息,请参阅 Einar 的帖子。
解决方案中的主要类是 NotifyingObjectWeaver
类,该类支持两个泛型方法来从基类获取代理实例
public T CreateInstance<T>()
public T CreateInstance<T>(T source)
可以通过以下方式获取代理类型
public Type GetProxyType<T>()
或
public Type GetProxyType(Type baseType)
以便设置 IoC 容器。
代理织工可以构建一个新实例或一个基于副本的实例,在本例中,测试获取了一个基于副本的实例
Foo myFooCopy = weaver.CreateInstance(myFoo);
为了订阅更改通知,必须获取 INotifyPropertyChanged 接口
(myFooCopy as INotifyPropertyChanged).PropertyChanged +=
(s, e) => System.Console.WriteLine(string.Format("Changed :{0}", e.PropertyName));
为了访问 IsDirty
标志和 Save()
方法,必须获取 ICanBeDirty
接口的引用,该接口很简单:
public interface ICanBeDirty
{
bool IsDirty { get; }
void Save();
}
无需使用属性来标记依赖项,例如,Foo
中的这个属性...
public string Name2
{
get
{
return Name1 + Date1.ToString();
}
}
...会在生成的代理类中产生此输出
public override string Name1
{
get { return base.Name1; }
set
{
if (!Equals(base.Name1, value))
{
base.Name1 = value;
OnPropertyChanged("Name1");
OnPropertyChanged("Name2");
}
}
}
public override DateTime Date1
{
get { return base.Date1; }
set
{
if (base.Date1 != value)
{
base.Date1 = value;
OnPropertyChanged("Date1");
OnPropertyChanged("Name2");
}
}
}
因为代理生成器会自动分析源代码以查找依赖项。
在 Einar 的原始解决方案中,依赖项由 NotifyChangesForAttribute
管理,该属性添加到基类
[NotifyChangesFor("Boolean1", "Name2")]
public virtual object Value1 { get; set; }
当 Value1
更改时,此属性允许引发 Boolean1
和 Name2
的更改。此解决方案不支持此属性。
还有一个 IgnoreChangesAttribute
属性,用于在属性更改时不要引发更改事件。
其余代码很简单;运行此演示,输出如下

关注点
NotifyingObjectWeaver
类对基类施加了一些限制
- 基类必须有一个默认构造函数才能实现复制构造函数
- 只有具有虚拟
public
set 的成员才能引发更改通知
测试方法 ProxyWeaverPerformanceTest
生成了以下结果
实例数 | 使用 new 的时间 (毫秒) | 使用 Proxy weaver 的时间 (毫秒) |
---|---|---|
1000 | 0 | 0 |
10000 | 0 | 0 |
100000 | 31 | 62 |
1000000 | 281 | 546 |
10000000 | 3078 | 5437 |
在正常情况下,例如将结果列表绑定到 datagrid
,网格最多可以显示 10 到 100 个项目,这意味着性能损失在这种情况下并非真正问题。
另一个有趣的特性是自动处理相关属性的更改通知,Simon Crop 使用 Mono.Cecil
实现了一个出色的解决方案,在这个解决方案中,“分析引擎”是使用 Acid Framework 中的 IL 读取器代码构建的。
代理织工的代码,包括查找相关属性的代码分析器,很复杂,源代码附在此处,欢迎提出建议和评论!!!
接下来是什么?
还有很多事情要做,首先,在下一篇文章中,我们将通过一个 WPF 的 caliburn micro 演示展示如何将代理织工与视图模型和 ProxyWrapper
类集成,但计划为织工添加一些新功能
- 一个流畅的配置,以支持忽略属性的更改并选择支持的接口
- 为
INotifyPropertyChanges
实现一个扩展,以引发所有属性的更改,获取属性的之前/之后值,并开启/关闭通知 - 实现
IEditableObject
- 重构以使用新的代码生成器
- 本地化错误消息(法语)
- 解决嵌套类和数据绑定的问题
- 集成更多单元测试