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

INotifyPropertyChanged - 自动实现( 重装)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (16投票s)

2011年11月18日

CPOL

4分钟阅读

viewsIcon

55450

downloadIcon

676

使用代理生成器实现 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 更改时,此属性允许引发 Boolean1Name2 的更改。此解决方案不支持此属性。
还有一个 IgnoreChangesAttribute 属性,用于在属性更改时不要引发更改事件。

其余代码很简单;运行此演示,输出如下

Sample output from ProxyWeaverTest method

关注点

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
  • 重构以使用新的代码生成器
  • 本地化错误消息(法语)
  • 解决嵌套类和数据绑定的问题
  • 集成更多单元测试
© . All rights reserved.