Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

INotifyPropertyChanged - Automagically Implemented (Reloaded)

0.00/5 (No votes)
25 Nov 2011 1  
Implement INotifyPropertyChanged and change verifying in model using a proxy generator

Introduction

This article is about implementing the INotifyPropertyChanged interface automatically with a new interface ICanBeDirty to detect if the model has changes using a custom generated proxy. Typically, INotifyPropertyChanged and change detecting are useful in WPF binding scenarios. There is an article from Simon Cropp describing many possible solutions to the problem; Simon himself proposes a solution injecting INotifyPropertyChanged code into properties at compile time.

This solution is based on the excellent work done by Einar Ingebrigtsen, he proposed a ProxyGenerator to transform this...

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; }
    }

...into something like this...

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");
        }
    }
}

Why to extend the Einar's solution? Mainly because in a WPF binding scenario, developers often need to have some kind of change verification; this solution extends the previous one adding "dirty checking" implementing the ICanBeDirty interface, a copy constructor and automatic PropertyChanged events for related properties, that means the developer does not need to setup manually the dependences in a property. This solution has an optimization to check if the new value is different from the old one before raising the event. This solution is intended for WPF binding, in consequence the performance lost to instantiate the proxy is not important, in the Points of Interest section, some performance tests results will be presented.

Using the Code

This solution lets the developer write this kind of 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();
    }

Note at first the set-up for the DispatcherManager in order to make the proxy "Dispatcher" friendly, for more details, see this Einar's post.

The main class in the solution is the NotifyingObjectWeaver class, this class supports two generics methods to get a proxy instance from a base class:

  • public T CreateInstance<T>()
  • public T CreateInstance<T>(T source)

The proxy type can be obtained with:

  • public Type GetProxyType<T>()

    or:

  • public Type GetProxyType(Type baseType)

in order to set-up an IoC container.

The proxy weaver can build a new instance or a copy based instance, in this case the test gets a copy based instance:

Foo myFooCopy = weaver.CreateInstance(myFoo); 

In order to subscribe to change notification, it is necessary to get the INotifyPropertyChanged interface:

(myFooCopy as INotifyPropertyChanged).PropertyChanged +=
(s, e) => System.Console.WriteLine(string.Format("Changed :{0}", e.PropertyName));

It is necessary to get a reference to the ICanBeDirty interface in order to access the IsDirty flag and the Save() method, this interface is simple:

public interface ICanBeDirty
    {
        bool IsDirty { get; }
        void Save();
    }

It is not necessary to use attributes to mark dependencies, by example, this property in Foo...

public string Name2
        {
            get
            {
                return Name1 + Date1.ToString();
            }
        }

...generates this output in the proxy generated class:

 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");
            }
        }
    }

Because the proxy generator analyses the source code to find dependencies automatically.
In the original Einar's solution, dependencies are managed by the NotifyChangesForAttribute adding this attribute to the base class:

[NotifyChangesFor("Boolean1", "Name2")]
public virtual object Value1 { get; set; }

This attribute lets raise changes for Boolean1 and Name2 when Value1 is changed. This solution does not support this attribute.
There is another attribute IgnoreChangesAttribute for not raising change events when a property is changed.

The rest of the code is straightforward; running this demo, the output is:

Sample output from ProxyWeaverTest method

Points of Interest

The NotifyingObjectWeaver class imposes some restrictions to the base class:

  • The base class must have a default constructor in order to implement a copy constructor
  • Only the members having a virtual public set can raise change notifications

The test method ProxyWeaverPerformanceTest generates these results:

Number of instances Time with new (msc) Time with Proxy weaver (msc)
1000 0 0
10000 0 0
100000 31 62
1000000 281 546
10000000 3078 5437

In the normal case, by example binding a result list with a datagrid, the grid can display at most 10 to 100 items, which means the performance lost is not really an issue in this case.

Another interesting feature to explain is the automatic change notification for related properties, Simon Crop has implemented an excellent solution using Mono.Cecil, and in this solution the "analysis engine" was built using the IL reader code in the Acid Framework.

The code for the proxy weaver, including the code analyzer to find related properties is complex, the source is attached here, suggestions and comments are very welcome!!!

What is Next?

There are a lot of things still to do, at first, in the next post a demo with caliburn micro in WPF will show how to integrate the proxy weaver with the view model and the ProxyWrapper class, but some new features are planned for the weaver:

  • A fluent configuration to support ignore changes for property and choose the supported interfaces
  • Implement an extension for INotifyPropertyChanges to raise changes for all properties, get before/after property values and turn on/off the notification
  • Implement IEditableObject
  • A refactoring to use a new code Emitter
  • Localize the error messages (French)
  • Solve the problem of nested classes and databinding
  • Integrate more unit tests

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here