Click here to Skip to main content
15,867,141 members
Articles / Desktop Programming / WPF

INotifyPropertyChanged - Automagically Implemented (Reloaded)

Rate me:
Please Sign up or sign in to vote.
4.83/5 (17 votes)
25 Nov 2011CPOL4 min read 52.4K   655   46   21
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...

C#
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...

C#
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:

C#
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:

C#
(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:

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

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

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

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

C#
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:

C#
[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 instancesTime with new (msc)Time with Proxy weaver (msc)
100000
1000000
1000003162
1000000281546
1000000030785437

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Fujitsu Canada
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
wvd_vegt26-Dec-11 3:09
professionalwvd_vegt26-Dec-11 3:09 
QuestionWhy does the proxy not set the same property on the source object? Pin
theperm24-Dec-11 6:54
theperm24-Dec-11 6:54 
AnswerRe: Why does the proxy not set the same property on the source object? Pin
emardini25-Dec-11 10:00
emardini25-Dec-11 10:00 
GeneralMy vote of 5 Pin
Kanasz Robert29-Nov-11 22:50
professionalKanasz Robert29-Nov-11 22:50 
GeneralRe: My vote of 5 Pin
emardini30-Nov-11 5:53
emardini30-Nov-11 5:53 
QuestionWhat do you think about merge INotifyPropertyChanged and IEditableObject? Pin
emardini28-Nov-11 4:34
emardini28-Nov-11 4:34 
QuestionOk, but... Pin
SledgeHammer0126-Nov-11 8:42
SledgeHammer0126-Nov-11 8:42 
AnswerRe: Ok, but... Pin
emardini27-Nov-11 16:27
emardini27-Nov-11 16:27 
Questionhow to implement the Save method? Pin
daview25-Nov-11 15:31
daview25-Nov-11 15:31 
AnswerRe: how to implement the Save method? Pin
emardini25-Nov-11 16:44
emardini25-Nov-11 16:44 
GeneralMy vote of 5 Pin
freakyit25-Nov-11 13:29
freakyit25-Nov-11 13:29 
GeneralRe: My vote of 5 Pin
emardini26-Nov-11 2:02
emardini26-Nov-11 2:02 
GeneralMy vote of 4 Pin
cdkisa25-Nov-11 12:38
cdkisa25-Nov-11 12:38 
GeneralRe: My vote of 4 Pin
emardini26-Nov-11 2:02
emardini26-Nov-11 2:02 
QuestionI did one of these a while back, may be of interest Pin
Sacha Barber23-Nov-11 21:48
Sacha Barber23-Nov-11 21:48 
AnswerRe: I did one of these a while back, may be of interest Pin
emardini24-Nov-11 3:29
emardini24-Nov-11 3:29 
QuestionProperty Dependancies, I dont get how they are setup? Pin
theperm22-Nov-11 23:27
theperm22-Nov-11 23:27 
AnswerRe: Property Dependancies, I dont get how they are setup? Pin
Eduardo Mardini23-Nov-11 3:32
Eduardo Mardini23-Nov-11 3:32 
QuestionSome updates coming Pin
emardini22-Nov-11 16:17
emardini22-Nov-11 16:17 
I will post some changes to the article, there is some junk code to delete, I will add code comments and unit tests.
GeneralMy vote of 5 Pin
VallarasuS20-Nov-11 6:26
VallarasuS20-Nov-11 6:26 
GeneralRe: My vote of 5 Pin
emardini26-Nov-11 2:03
emardini26-Nov-11 2:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.