Click here to Skip to main content
Click here to Skip to main content

INotifyPropertyChanged and Beyond - Part I

, 7 May 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
Improving and extending the INotifyPropertyChanged interface
Screenshot - screenshot.png

Introduction

The INotifyPropertyChanged interface provides a standard event for objects to notify clients that one of its properties has changed. This is helpful for data binding (as described in this article), since it allows for bound controls to update their display based on changes made directly to the underlying object. While this event serves its purpose, there is still room for improvement.

This article goes over some improvements and extensions to this interface. We will start with easing the use of INotifyPropertyChanged and add new features as we go along. A new interface, called IPropertyNotification, will be used in order to extend the INotifyPropertyChanged interface.

IPropertyNotification Interface and a Base Class

To begin, the IPropertyNotification interface will simply be defined as:

public interface IPropertyNotification : INotifyPropertyChanged {
    // No members
}

We will then define a base class as follows, which will allow our derived objects to easily utilize this interface:

/// <span class="code-SummaryComment"><summary></span>
/// This class implements the <span class="code-SummaryComment"><see cref="T:IPropertyNotification"/></span>
/// interface and provides helper methods for derived classes.
/// <span class="code-SummaryComment"></summary></span>
public class PropertyNotificationObject : IPropertyNotification {
    #region IPropertyNotification

    /// <span class="code-SummaryComment"><summary></span>
    /// Occurs when a property value changes.
    /// <span class="code-SummaryComment"></summary></span>
    [field:NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    #endregion // IPropertyNotification

    #region Methods

    /// <span class="code-SummaryComment"><summary></span>
    /// Raises the <span class="code-SummaryComment"><see cref="E:PropertyChanged"/> event.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="propertyName"></span>
    /// Name of the property that changed.
    /// <span class="code-SummaryComment"></param></span>
    protected void OnPropertyChanged(String propertyName) {
        PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
        OnPropertyChanged(e);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Raises the <span class="code-SummaryComment"><see cref="E:PropertyChanged"/> event.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="e"></span>
    /// The <span class="code-SummaryComment"><see cref="PropertyChangedEventArgs"/> instance</span>
    /// containing the event data.
    /// <span class="code-SummaryComment"></param></span>
    protected void OnPropertyChanged(PropertyChangedEventArgs e) {
        PropertyChangedEventHandler temp = this.PropertyChanged;
        if (null != temp)
            temp(this, e);
    }

    #endregion // Methods
}

As you can see, the base class allows derived classes to easily call the PropertyChanged event. As shown here:

/// <span class="code-SummaryComment"><summary></span>
/// This class is used to test the functionality of
/// <span class="code-SummaryComment"><see cref="T:PropertyNotificationObject"/> and</span>
/// <span class="code-SummaryComment"><see cref="T:IPropertyNotification"/>.</span>
/// <span class="code-SummaryComment"></summary></span>
public class TestObject : PropertyNotificationObject {
    #region Properties

    /// <span class="code-SummaryComment"><summary></span>
    /// Holds the name.
    /// <span class="code-SummaryComment"></summary></span>
    private String name = String.Empty;

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the name.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><value>The name.</value></span>
    public String Name {
        get {
            return this.name;
        }
        set {
            if (false == Object.Equals(value, this.name)) {
                this.name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    #endregion // Properties
}

Now that the base class and interface are defined, we will start to add some new functionality.

What Changed?

When the PropertyChanged event is fired, only the name of the changed property is provided. In certain cases, it would be useful to know the previous value and the new value of the property. The new value could be obtained using the information currently available in the event. Specifically, reflection could be used on the sender (assuming the sender is the object that changed) to get the value of the property with the name specified by the event. This tends to get messy though and does not allow us to get the previous value.

Instead of using reflection, we will implement a class derived from PropertyChangedEventArgs that will carry the old and new values. This class is shown below:

/// <span class="code-SummaryComment"><summary></span>
/// This class extends <span class="code-SummaryComment"><see cref="T:PropertyChangedEventArgs"/> and</span>
/// allows for storing the old and new values of the changed property.
/// <span class="code-SummaryComment"></summary></span>
public class PropertyNotificationEventArgs : PropertyChangedEventArgs {
    #region Constructors

    /// <span class="code-SummaryComment"><summary></span>
    /// Initializes a new instance of the
    /// <span class="code-SummaryComment"><see cref="PropertyNotificationEventArgs"/> class.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="propertyName"></span>
    /// The name of the property that is associated with this
    /// notification.
    /// <span class="code-SummaryComment"></param></span>
    public PropertyNotificationEventArgs(String propertyName)
        : this(propertyName, null, null) {
        // No-op
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Initializes a new instance of the
    /// <span class="code-SummaryComment"><see cref="PropertyNotificationEventArgs"/> class.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="propertyName"></span>
    /// The name of the property that is associated with this
    /// notification.
    /// <span class="code-SummaryComment"></param></span>
    /// <span class="code-SummaryComment"><param name="oldValue">The old value.</param></span>
    /// <span class="code-SummaryComment"><param name="newValue">The new value.</param></span>
    public PropertyNotificationEventArgs(String propertyName,
        Object oldValue, Object newValue)
        : base(propertyName) {
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    #endregion // Constructors

    #region Properties

    /// <span class="code-SummaryComment"><summary></span>
    /// Holds the new value of the property.
    /// <span class="code-SummaryComment"></summary></span>
    private Object newValue;

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets the new value of the property.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><value>The new value.</value></span>
    public Object NewValue {
        get {
            return this.newValue;
        }
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Holds the old value of the property.
    /// <span class="code-SummaryComment"></summary></span>
    private Object oldValue;

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets the old value of the property.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><value>The old value.</value></span>
    public Object OldValue {
        get {
            return this.oldValue;
        }
    }

    #endregion // Properties
}

In order to use this new class, we must modify our base class and our test object, as shown here:

/// <span class="code-SummaryComment"><summary></span>
/// This class implements the <span class="code-SummaryComment"><see cref="T:IPropertyNotification"/></span>
/// interface and provides helper methods for derived classes.
/// <span class="code-SummaryComment"></summary></span>
public class PropertyNotificationObject : IPropertyNotification {
    // ... Existing code ...

    #region Methods

    /// <span class="code-SummaryComment"><summary></span>
    /// Raises the <span class="code-SummaryComment"><see cref="E:PropertyChanged"/> event.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="propertyName"></span>
    /// Name of the property that changed.
    /// <span class="code-SummaryComment"></param></span>
    /// <span class="code-SummaryComment"><param name="oldValue">The old value.</param></span>
    /// <span class="code-SummaryComment"><param name="newValue">The new value.</param></span>
    protected void OnPropertyChanged(String propertyName,
        Object oldValue, Object newValue) {
        PropertyNotificationEventArgs e = 
	new PropertyNotificationEventArgs(propertyName,
            oldValue, newValue); // ** Pass in the old and new value
        OnPropertyChanged(e);
    }

    // ... Existing code ...

    #endregion // Methods
}

/// <span class="code-SummaryComment"><summary></span>
/// This class is used to test the functionality of
/// <span class="code-SummaryComment"><see cref="T:PropertyNotificationObject"/> and</span>
/// <span class="code-SummaryComment"><see cref="T:IPropertyNotification"/>.</span>
/// <span class="code-SummaryComment"></summary></span>
public class TestObject : PropertyNotificationObject {
    #region Properties

    // ... Existing code ...

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the name.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><value>The name.</value></span>
    public String Name {
        // ... Existing code ...
        set {
            if (false == Object.Equals(value, this.name)) {
                String oldValue = this.name; // ** Save the old value
                this.name = value;
	       // ** Pass in the old and new value
                OnPropertyChanged("Name", oldValue, this.name); 
            }
        }
    }

    #endregion // Properties
}

One problem with this approach is that clients must use the "is" or "as" operators to determine if the given PropertyChangedEventArgs is actually an instance of PropertyNotificationEventArgs. We could overcome this problem by creating a new event in our IPropertyNotification interface which takes an instance of PropertyNotificationEventArgs, but then we lose the seamless integration with clients that already support INotifyPropertyChanged.

Cancel A Change

There are many scenarios where simply getting an event after a property has changed is sufficient. There are also many scenarios where a cancellable event before a property has changed is required (e.g. Source Control, Validation, etc). In order to better support these types of scenarios, we will add a PropertyChanging event to our interface. First, we need to define a class derived from PropertyNotificationEventArgs that allows us to cancel the event. This class is shown below:

/// <span class="code-SummaryComment"><summary></span>
/// This class extends <span class="code-SummaryComment"><see cref="T:PropertyNotificationEventArgs"/> and</span>
/// allows for cancelling of the associated event.
/// <span class="code-SummaryComment"></summary></span>
public class CancelPropertyNotificationEventArgs : PropertyNotificationEventArgs {
    #region Constructors

    /// <span class="code-SummaryComment"><summary></span>
    /// Initializes a new instance of the
    /// <span class="code-SummaryComment"><see cref="CancelPropertyNotificationEventArgs"/> class.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="propertyName"></span>
    /// The name of the property that is associated with this
    /// notification.
    /// <span class="code-SummaryComment"></param></span>
    public CancelPropertyNotificationEventArgs(String propertyName)
        : base(propertyName) {
        // No-op
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Initializes a new instance of the
    /// <span class="code-SummaryComment"><see cref="CancelPropertyNotificationEventArgs"/> class.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="propertyName"></span>
    /// The name of the property that is associated with this
    /// notification.
    /// <span class="code-SummaryComment"></param></span>
    /// <span class="code-SummaryComment"><param name="oldValue">The old value.</param></span>
    /// <span class="code-SummaryComment"><param name="newValue">The new value.</param></span>
    public CancelPropertyNotificationEventArgs(String propertyName,
        Object oldValue, Object newValue)
        : base(propertyName, oldValue, newValue) {
        // No-op
    }

    #endregion // Constructors

    #region Properties

    /// <span class="code-SummaryComment"><summary></span>
    /// Holds a value indicating whether the associated event should be
    /// cancelled.
    /// <span class="code-SummaryComment"></summary></span>
    private Boolean cancel = false;

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets a value indicating whether the associated event should
    /// be cancelled.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><value></span>
    /// <span class="code-SummaryComment"><c>true</c> if the event should be cancelled; otherwise, <c>false</c>.</span>
    /// <span class="code-SummaryComment"></value></span>
    public Boolean Cancel {
        get {
            return this.cancel;
        }
        set {
            this.cancel = value;
        }
    }

    #endregion // Properties
}

Next we will add a new event to our interface and the associated helper methods to our base class as shown here:

/// <span class="code-SummaryComment"><summary></span>
/// Notifies clients that a property value is changing or changed.
/// <span class="code-SummaryComment"></summary></span>
public interface IPropertyNotification : INotifyPropertyChanged {
    #region Events

    /// <span class="code-SummaryComment"><summary></span>
    /// Occurs when a property value is changing.
    /// <span class="code-SummaryComment"></summary></span>
    event PropertyChangingEventHandler PropertyChanging;

    #endregion // Events
}
/// <span class="code-SummaryComment"><summary></span>
/// This class implements the <span class="code-SummaryComment"><see cref="T:IPropertyNotification"/></span>
/// interface and provides helper methods for derived classes.
/// <span class="code-SummaryComment"></summary></span>
public class PropertyNotificationObject : IPropertyNotification {
    #region IPropertyNotification

    // ... Existing code ...

    /// <span class="code-SummaryComment"><summary></span>
    /// Occurs when a property value is changing.
    /// <span class="code-SummaryComment"></summary></span>
    [field: NonSerialized]
    public event PropertyChangingEventHandler PropertyChanging;

    #endregion // IPropertyNotification

    #region Methods

    // ... Existing code ...

    /// <span class="code-SummaryComment"><summary></span>
    /// Raises the <span class="code-SummaryComment"><see cref="E:PropertyChanging"/> event.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="propertyName"></span>
    /// Name of the property that is changing.
    /// <span class="code-SummaryComment"></param></span>
    /// <span class="code-SummaryComment"><param name="oldValue">The old value.</param></span>
    /// <span class="code-SummaryComment"><param name="newValue">The new value.</param></span>
    /// <span class="code-SummaryComment"><returns><c>true</c> if the change can continue; </span>
    /// otherwise <span class="code-SummaryComment"><c>false</c>.</returns></span>
    protected Boolean OnPropertyChanging(String propertyName,
        Object oldValue, Object newValue) {
        CancelPropertyNotificationEventArgs e = 
			new CancelPropertyNotificationEventArgs(propertyName,
            oldValue, newValue);
        OnPropertyChanging(e);
        return !e.Cancel;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Raises the <span class="code-SummaryComment"><see cref="E:PropertyChanging"/> event.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="e"></span>
    /// The <span class="code-SummaryComment"><see cref="CancelPropertyNotificationEventArgs"/> instance</span>
    /// containing the event data.
    /// <span class="code-SummaryComment"></param></span>
    protected void OnPropertyChanging(CancelPropertyNotificationEventArgs e) {
        PropertyChangingEventHandler temp = this.PropertyChanging;
        if (null != temp)
            temp(this, e);
    }

    #endregion // Methods
}

Finally, we can hook up the new event to our test object like so:

/// <span class="code-SummaryComment"><summary></span>
/// This class is used to test the functionality of
/// <span class="code-SummaryComment"><see cref="T:PropertyNotificationObject"/> and</span>
/// <span class="code-SummaryComment"><see cref="T:IPropertyNotification"/>.</span>
/// <span class="code-SummaryComment"></summary></span>
public class TestObject : PropertyNotificationObject {
    #region Properties

    // ... Existing code ...

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the name.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><value>The name.</value></span>
    public String Name {
        // ... Existing code ...
        set {
            if (false == Object.Equals(value, this.name)) {
                if (true == OnPropertyChanging("Name", this.name, value)) 
	        { // ** Call Changing event
                    String oldValue = this.name;
                    this.name = value;
                    OnPropertyChanged("Name", oldValue, this.name);
                }
            }
        }
    }

    #endregion // Properties
}

Using this new method, it is now possible to cancel changes on an instance of TestObject. This includes changes from the PropertyGrid, a data bound control, or from direct access.

Boiler-plate Set Code

We have abstracted out the code from the Property set method into a helper method in the base class. While this may handle most cases, there will still be instances where this helper method will not work. The new helper method is shown here:

/// <span class="code-SummaryComment"><summary></span>
/// This method is used to set a property while firing associated
/// PropertyChanging and PropertyChanged events.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="propertyName">Name of the property.</param></span>
/// <span class="code-SummaryComment"><param name="propertyField">The property field.</param></span>
/// <span class="code-SummaryComment"><param name="value">The value.</param></span>
protected void SetProperty<T>(String propertyName, ref T propertyField,
    T value) {
    if (false == Object.Equals(value, propertyField)) {
        if (true == OnPropertyChanging(propertyName, propertyField, value)) {
            T oldValue = propertyField;
            propertyField = value;
            OnPropertyChanged(propertyName, oldValue, propertyField);
        }
    }
}

And this is how it is used by the TestObject:

/// <span class="code-SummaryComment"><summary></span>
/// This class is used to test the functionality of
/// <span class="code-SummaryComment"><see cref="T:PropertyNotificationObject"/> and</span>
/// <span class="code-SummaryComment"><see cref="T:IPropertyNotification"/>.</span>
/// <span class="code-SummaryComment"></summary></span>
public class TestObject : PropertyNotificationObject {
    #region Properties

    // ... Existing code ...

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the name.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><value>The name.</value></span>
    public String Name {
        // ... Existing code ...
        set {
            SetProperty<String>("Name", ref this.name, value);
        }
    }

    #endregion // Properties
}

Wrap Up

We have started to build base code that will allow our applications to have finer control over changes to our objects. These events are very helpful when using third-party controls, which do not allow similar control.

The demo application contains all the completed code from this article and a simple example that shows it in action.

In the next part of this article, we will tackle the following improvements:

  1. Propagation support – This allows an object to propagate its PropertyChanged/PropertyChanging events to its parent, when objects are organized in a hierarchical fashion (e.g. parent/child). This allows client applications to hook up a single event handler in order to receive any change notifications.
  2. Batch Change support – When lots of properties are being updated, it may be desirable to group these changes into a single event. In this case, we would like to know all the properties that changed and how they changed.
  3. Event suppression – There may be cases where we would like to suppress these events altogether. So we will add support for turning off the events.

History

  • 7th May, 2007: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

TJoe
Chief Technology Officer SQL Farms, Inc.
United States United States
My name is Tom Goff and I have been working as a Software Engineer for over 15 years. Over my career, I have primarily focused on Windows programming with C++ and C#. I have also worked extensively with Microsoft SQL Server over the past 6 years.

Comments and Discussions

 
QuestionHow to implement INotifyPropertyChanged right? Pinmemberjbe822418-Apr-09 11:03 
GeneralNo "Notify Test" appears in the output of this project executed in Visual C#.NET 2005 Express!!?? [modified] PinmemberSctt H. Chang19-Nov-08 4:42 
Hi TJoe,
I read your article to learn "INotifyPropertyChanged" and downloaded your demo project to my PC. I executed your "NotifyTest" project in my Visual C#.NET 2005 Express. It generated the output of "Form1" with 'Misc' and 'Name'. If I clicked on the 'Misc' or 'Name', I saw 'Misc' or 'Name' appeared in the left-hand-side bottom. But I did not get the box of "Notify Test" Would you like to change the property 'Name' from 'Testing' to 'More Testing? |Yes| |No| in my computer screen!!?? I am new in the C# programming and "INotifyPropertyChanged" is too complicated and hard for me to understand and use it. Could you please tell me why I did not get the "Notify Test" box and how to fix it?
Thanks in advance,
Scott Chang
 
modified on Wednesday, November 19, 2008 10:51 AM

QuestionCancelling the Event? PinmemberSteveC-A97-Oct-08 11:01 
GeneralExcellent Article Pinmembermathewrajiv4-May-08 20:33 
GeneralNice Pinmemberelektrowolf25-Apr-08 7:30 
GeneralRe: Nice Pinmemberelektrowolf25-Apr-08 7:35 
GeneralFor serialization supporting PinmemberYanhua Zhang21-Jan-08 23:12 
GeneralINotifyPropertyChanging in .Net 3.5 PinmemberTJoe18-Oct-07 8:10 
GeneralFixing Deserialization PinmemberMike Gavaghan22-Jul-07 7:44 
GeneralRe: Fixing Deserialization PinmemberTJoe22-Jul-07 9:42 
Questionfalse == ... false? PinmemberSteve Hansen7-May-07 21:51 
AnswerRe: false == ... false? PinmemberTJoe8-May-07 2:20 
GeneralRe: false == ... false? Pinmemberelektrowolf25-Apr-08 7:38 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.141022.2 | Last Updated 7 May 2007
Article Copyright 2007 by TJoe
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid