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 {
}
We will then define a base class as follows, which will allow our derived objects to easily utilize this interface:
public class PropertyNotificationObject : IPropertyNotification {
#region IPropertyNotification
[field:NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Methods
protected void OnPropertyChanged(String propertyName) {
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
}
protected void OnPropertyChanged(PropertyChangedEventArgs e) {
PropertyChangedEventHandler temp = this.PropertyChanged;
if (null != temp)
temp(this, e);
}
#endregion
}
As you can see, the base class allows derived classes to easily call the PropertyChanged
event. As shown here:
public class TestObject : PropertyNotificationObject {
#region Properties
private String name = String.Empty;
public String Name {
get {
return this.name;
}
set {
if (false == Object.Equals(value, this.name)) {
this.name = value;
OnPropertyChanged("Name");
}
}
}
#endregion
}
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:
public class PropertyNotificationEventArgs : PropertyChangedEventArgs {
#region Constructors
public PropertyNotificationEventArgs(String propertyName)
: this(propertyName, null, null) {
}
public PropertyNotificationEventArgs(String propertyName,
Object oldValue, Object newValue)
: base(propertyName) {
this.oldValue = oldValue;
this.newValue = newValue;
}
#endregion
#region Properties
private Object newValue;
public Object NewValue {
get {
return this.newValue;
}
}
private Object oldValue;
public Object OldValue {
get {
return this.oldValue;
}
}
#endregion
}
In order to use this new class, we must modify our base class and our test object, as shown here:
public class PropertyNotificationObject : IPropertyNotification {
#region Methods
protected void OnPropertyChanged(String propertyName,
Object oldValue, Object newValue) {
PropertyNotificationEventArgs e =
new PropertyNotificationEventArgs(propertyName,
oldValue, newValue);
OnPropertyChanged(e);
}
#endregion
}
public class TestObject : PropertyNotificationObject {
#region Properties
public String Name {
set {
if (false == Object.Equals(value, this.name)) {
String oldValue = this.name;
this.name = value;
OnPropertyChanged("Name", oldValue, this.name);
}
}
}
#endregion
}
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:
public class CancelPropertyNotificationEventArgs : PropertyNotificationEventArgs {
#region Constructors
public CancelPropertyNotificationEventArgs(String propertyName)
: base(propertyName) {
}
public CancelPropertyNotificationEventArgs(String propertyName,
Object oldValue, Object newValue)
: base(propertyName, oldValue, newValue) {
}
#endregion
#region Properties
private Boolean cancel = false;
public Boolean Cancel {
get {
return this.cancel;
}
set {
this.cancel = value;
}
}
#endregion
}
Next we will add a new event to our interface and the associated helper methods to our base class as shown here:
public interface IPropertyNotification : INotifyPropertyChanged {
#region Events
event PropertyChangingEventHandler PropertyChanging;
#endregion
}
public class PropertyNotificationObject : IPropertyNotification {
#region IPropertyNotification
[field: NonSerialized]
public event PropertyChangingEventHandler PropertyChanging;
#endregion
#region Methods
protected Boolean OnPropertyChanging(String propertyName,
Object oldValue, Object newValue) {
CancelPropertyNotificationEventArgs e =
new CancelPropertyNotificationEventArgs(propertyName,
oldValue, newValue);
OnPropertyChanging(e);
return !e.Cancel;
}
protected void OnPropertyChanging(CancelPropertyNotificationEventArgs e) {
PropertyChangingEventHandler temp = this.PropertyChanging;
if (null != temp)
temp(this, e);
}
#endregion
}
Finally, we can hook up the new event to our test object like so:
public class TestObject : PropertyNotificationObject {
#region Properties
public String Name {
set {
if (false == Object.Equals(value, this.name)) {
if (true == OnPropertyChanging("Name", this.name, value))
{
String oldValue = this.name;
this.name = value;
OnPropertyChanged("Name", oldValue, this.name);
}
}
}
}
#endregion
}
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:
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
:
public class TestObject : PropertyNotificationObject {
#region Properties
public String Name {
set {
SetProperty<String>("Name", ref this.name, value);
}
}
#endregion
}
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:
- 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.
- 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.
- 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