Click here to Skip to main content
15,916,215 members
Articles / Programming Languages / C#
Article

INotifyPropertyChanged and Beyond - Part I

Rate me:
Please Sign up or sign in to vote.
4.80/5 (22 votes)
7 May 2007CPOL4 min read 71.5K   937   72   14
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:

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

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

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    [field:NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    #endregion // IPropertyNotification

    #region Methods

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

    /// <summary>
    /// Raises the <see cref="E:PropertyChanged"/> event.
    /// </summary>
    /// <param name="e">
    /// The <see cref="PropertyChangedEventArgs"/> instance
    /// containing the event data.
    /// </param>
    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:

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

    /// <summary>
    /// Holds the name.
    /// </summary>
    private String name = String.Empty;

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    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:

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

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

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

    #endregion // Constructors

    #region Properties

    /// <summary>
    /// Holds the new value of the property.
    /// </summary>
    private Object newValue;

    /// <summary>
    /// Gets the new value of the property.
    /// </summary>
    /// <value>The new value.</value>
    public Object NewValue {
        get {
            return this.newValue;
        }
    }

    /// <summary>
    /// Holds the old value of the property.
    /// </summary>
    private Object oldValue;

    /// <summary>
    /// Gets the old value of the property.
    /// </summary>
    /// <value>The old value.</value>
    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:

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

    #region Methods

    /// <summary>
    /// Raises the <see cref="E:PropertyChanged"/> event.
    /// </summary>
    /// <param name="propertyName">
    /// Name of the property that changed.
    /// </param>
    /// <param name="oldValue">The old value.</param>
    /// <param name="newValue">The new value.</param>
    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
}

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

    // ... Existing code ...

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    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:

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

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

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

    #endregion // Constructors

    #region Properties

    /// <summary>
    /// Holds a value indicating whether the associated event should be
    /// cancelled.
    /// </summary>
    private Boolean cancel = false;

    /// <summary>
    /// Gets or sets a value indicating whether the associated event should
    /// be cancelled.
    /// </summary>
    /// <value>
    /// <c>true</c> if the event should be cancelled; otherwise, <c>false</c>.
    /// </value>
    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:

C#
/// <summary>
/// Notifies clients that a property value is changing or changed.
/// </summary>
public interface IPropertyNotification : INotifyPropertyChanged {
    #region Events

    /// <summary>
    /// Occurs when a property value is changing.
    /// </summary>
    event PropertyChangingEventHandler PropertyChanging;

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

    // ... Existing code ...

    /// <summary>
    /// Occurs when a property value is changing.
    /// </summary>
    [field: NonSerialized]
    public event PropertyChangingEventHandler PropertyChanging;

    #endregion // IPropertyNotification

    #region Methods

    // ... Existing code ...

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

    /// <summary>
    /// Raises the <see cref="E:PropertyChanging"/> event.
    /// </summary>
    /// <param name="e">
    /// The <see cref="CancelPropertyNotificationEventArgs"/> instance
    /// containing the event data.
    /// </param>
    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:

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

    // ... Existing code ...

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    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:

C#
/// <summary>
/// This method is used to set a property while firing associated
/// PropertyChanging and PropertyChanged events.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
/// <param name="propertyField">The property field.</param>
/// <param name="value">The value.</param>
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:

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

    // ... Existing code ...

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>The name.</value>
    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)


Written By
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

 
GeneralMy vote of 5 Pin
BillWoodruff15-May-21 18:06
professionalBillWoodruff15-May-21 18:06 
QuestionHow to implement INotifyPropertyChanged right? Pin
jbe822418-Apr-09 11:03
jbe822418-Apr-09 11:03 
GeneralNo "Notify Test" appears in the output of this project executed in Visual C#.NET 2005 Express!!?? [modified] Pin
Sctt H. Chang19-Nov-08 4:42
Sctt H. Chang19-Nov-08 4:42 
QuestionCancelling the Event? Pin
IgDev7-Oct-08 11:01
IgDev7-Oct-08 11:01 
GeneralExcellent Article Pin
mathewrajiv4-May-08 20:33
mathewrajiv4-May-08 20:33 
GeneralNice Pin
elektrowolf25-Apr-08 7:30
elektrowolf25-Apr-08 7:30 
GeneralRe: Nice Pin
elektrowolf25-Apr-08 7:35
elektrowolf25-Apr-08 7:35 
GeneralFor serialization supporting Pin
Yanhua Zhang21-Jan-08 23:12
Yanhua Zhang21-Jan-08 23:12 
GeneralINotifyPropertyChanging in .Net 3.5 Pin
TJoe18-Oct-07 8:10
TJoe18-Oct-07 8:10 
GeneralFixing Deserialization Pin
Mike Gavaghan22-Jul-07 7:44
Mike Gavaghan22-Jul-07 7:44 
GeneralRe: Fixing Deserialization Pin
TJoe22-Jul-07 9:42
TJoe22-Jul-07 9:42 
Questionfalse == ... false? Pin
Steve Hansen7-May-07 21:51
Steve Hansen7-May-07 21:51 
AnswerRe: false == ... false? Pin
TJoe8-May-07 2:20
TJoe8-May-07 2:20 
GeneralRe: false == ... false? Pin
elektrowolf25-Apr-08 7:38
elektrowolf25-Apr-08 7:38 

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.