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.
In Part I of this series, we built some improvements into the INotifyPropertyChanged
interface. This article continues to build upon the code from Part I by adding additional improvements. We will extend our code to support event suppression and event propagation. In an attempt to keep this article from being excessively long, I will address batched events in a follow-up article.
Event suppression
Introduction
The PropertyChanged
and PropertyChanging
events are fired every time a property is changed, but there may be times when we do not want these events to be fired. For example, if you are loading data or if you have already verified that an object should be allowed to change, then you may not want these events to fire. To that end, we will update the code so that we can suppress the events.
Solution
To accomplish this, we will add two methods to our base class called SuspendPropertyEvents
and ResumePropertyEvents
. There are two ways we could implement these methods.
The first involves adding a private Boolean field member called propertyEventsSuspended
. We would set this field to true in SuspendPropertyEvents
and set it to false in ResumePropertyEvents
. Before firing PropertyChanged
or PropertyChanging
, we would verify that this field is set to false. If the field is set to true, then we would not fire the event(s). The problem with this approach is that you cannot have nested calls to these methods. For example:
TestObject test = new TestObject();
test.SuspendPropertyEvents();
try
{
test.SuspendPropertyEvents();
try
{
}
finally
{
test.resumePropertyEvents();
}
}
finally
{
test.resumePropertyEvents();
}
In this scenario, the events would start firing after the first call to ResumePropertyEvents
. Obviously the example above is contrived, but the inner try-finally block could easily be part of a method that is called to perform changes on the TestObject
or a method several levels removed. Basically, you as the coder must now be aware of whether any methods you call are going to call ResumePropertyEvents
.
The second implementation solves this by using an Int32 field initialized to zero (0), instead of a Boolean. This field is incremented by SuspendPropertyEvents
and decremented by ResumePropertyEvents
. Before firing the events, we verify that this field is set to zero (0). If the value is not zero (0), then we do not fire the events. This solves the problem of having nested called to SuspendPropertyEvents
and ResumePropertyEvents
.
The following code shows the base class with the latest updates. I only show the changes to one version of OnPropertyChanged
, but the same applies to all overloads of OnPropertyChanged
and OnPropertyChanging
:
public class PropertyNotificationObject : IPropertyNotification
{
#region Methods
protected void OnPropertyChanged(PropertyNotificationEventArgs e)
{
if (true == this.PropertyEventsSuspended)
return;
PropertyChangedEventHandler temp = this.PropertyChanged;
if (null != temp)
temp(this, e);
}
public void ResumePropertyEvents()
{
this.propertyEventSuspendCount--;
}
public void SuspendPropertyEvents()
{
this.propertyEventSuspendCount++;
}
#endregion
#region Properties/Fields
public Boolean PropertyEventsSuspended
{
get
{
return (0 != this.propertyEventSuspendCount);
}
}
private Int32 propertyEventSuspendCount = 0;
#endregion
}
Conclusion
This implementation introduces a slight problem if our objects are going to be accessed by multiple threads. If two threads were to call SuspendPropertyEvents
and/or ResumePropertyEvents
at the same time, then our counter would most likely get out of sync. Technically, the same problem would exist if we used a Boolean field, but we have already shown that the Boolean field is ineffective even with just a single thread. We will not address this problem here, but it could easily be corrected using the "lock" keyword.
Event propagation
Introduction
When working with properties that use immutable types, we can easily control when they are set. This also means we can easily fire the PropertyChanged
and PropertyChanging
events. This becomes a problem when the property type is mutable. With mutable property types, the underlying object can be changed without using the set accessor for the property on the main object. For example, in the code below the ChildObject
has its Name
property changed, but the ParentObject
is never notified:
public class ChildObject
{
#region Properties/Fields
private String name = String.Empty;
public String Name
{
get
{
return this.name;
}
set
{
this.name = value;
}
}
#endregion
}
public class ParentObject : PropertyNotificationObject
{
#region Constructors
public ParentObject()
{
this.Child = new ChildObject();
}
#endregion
#region Properties/Fields
private ChildObject child;
[TypeConverter(typeof(ExpandableObjectConverter))]
public ChildObject Child
{
get
{
return this.child;
}
set
{
SetProperty<ChildObject>("Child",
ref this.child, value);
}
}
private String name = String.Empty;
public String Name
{
get
{
return this.name;
}
set
{
SetProperty<String>("Name",
ref this.name, value);
}
}
#endregion
}
static class Program
{
[STAThread]
static void Main()
{
ParentObject parent = new ParentObject();
parent.PropertyChanged += new
PropertyChangedEventHandler(parent_PropertyChanged);
parent.Child.Name = "Testing";
}
private static void parent_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
}
}
This is where event propagation comes to the rescue. If we add support for IPropertyNotification
to the ChildObject
-- this code will not be shown here, since this type of change has already been given -- then the PropertyChanging
and PropertyChanged
events can be propagated through the ParentObject
.
Collections introduce a unique problem, as well. The .NET collections typically have a single settable property called Item,
which equates to the "this" accessor property in C#. In addition, Collections have numerous methods that modify the Item
property, e.g. Add
, Remove
, Clear
. This class design is not limited to Collections. Any class can define a method that sets one or more properties.
For this article, I will define a "simple class" as a class whose properties can only be set through the property definition, e.g. the properties are not modified by a method defined in the class. Therefore a "complex class" will be defined as a class whose properties can be set from the property definition or through methods. We will address each type separately.
Simple class solution
In order to hook up event propagation for simple classes, we need to do one of the following two things.
First, we could store a reference to the parent object in a new property in the sub-object or use some other mechanism of getting the parent object. This would require us to store additional information in the sub-object so that it can forward its events to its parent object. This means that the parent object must pass a reference to itself to its sub-objects when they are "added" to the parent.
The second option requires us to add event handlers to the sub-objects from the parent object. So, instead of the sub-object forwarding its events to the parent, the parent would listen to events from its sub-objects and forward them itself. We will implement this option because it is a bit cleaner. Instead of adding code to hook up each individual sub-object, we will add generic support to our base class and its SetProperty
method. The changes are shown here:
public class PropertyNotificationObject : IPropertyNotification
{
#region Methods
protected void AddPropertyEventHandlers(Object obj)
{
IPropertyNotification propNotify = obj as IPropertyNotification;
if (null != propNotify)
{
propNotify.PropertyChanged += new
PropertyChangedEventHandler(SubObject_PropertyChanged);
propNotify.PropertyChanging += new
PropertyChangingEventHandler(SubObject_PropertyChanging);
}
}
protected void OnPropertyChanged(Object sender,
PropertyChangedEventArgs e)
{
if (true == this.PropertyEventsSuspended)
return;
PropertyChangedEventHandler temp = this.PropertyChanged;
if (null != temp)
temp(sender, e);
}
protected void OnPropertyChanging(Object sender,
CancelPropertyNotificationEventArgs e)
{
if (true == this.PropertyEventsSuspended)
return;
PropertyChangingEventHandler temp = this.PropertyChanging;
if (null != temp)
temp(sender, e);
}
protected void RemovePropertyEventHandlers(Object obj)
{
IPropertyNotification propNotify = obj as IPropertyNotification;
if (null != propNotify)
{
propNotify.PropertyChanged -= new
PropertyChangedEventHandler(SubObject_PropertyChanged);
propNotify.PropertyChanging -= new
PropertyChangingEventHandler(SubObject_PropertyChanging);
}
}
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;
RemovePropertyEventHandlers(oldValue);
AddPropertyEventHandlers(propertyField);
OnPropertyChanged(propertyName, oldValue,
propertyField);
}
}
}
private void SubObject_PropertyChanged(Object sender,
PropertyChangedEventArgs e)
{
if (true == this.PropagatePropertyNotifications)
OnPropertyChanged(sender, e);
}
private void SubObject_PropertyChanging(object sender,
CancelPropertyNotificationEventArgs e)
{
if (true == this.PropagatePropertyNotifications)
OnPropertyChanging(sender, e);
}
#endregion
#region Properties/Fields
private Boolean propagatePropertyNotifications = true;
public Boolean PropagatePropertyNotifications
{
get
{
return this.propagatePropertyNotifications;
}
set
{
SetProperty<Boolean>(
"PropagatePropertyNotifications",
ref this.propagatePropertyNotifications, value);
}
}
#endregion
}
We have updated the SetProperty
method so that it calls RemoveEventHandlers
on the old property value and AddEventHandlers
on the new property value. These new methods determine if the given object supports the IPropertyNotification
interface. If it does, it will add or remove the event handlers. The event handlers will then forward or propagate any events from the sub-objects. This forwarding mechanism is controlled by a new Boolean property called PropagatePropertyNotifications
. Finally, we add overloaded methods for OnPropertyChanged
and OnPropertyChanging
that accept the sender as a parameter because we can no longer hard-code "this."
Now, assuming that ChildObject
above is updated to support the IPropertyNotification
interface, then the events from ChildObject
will be propagated to ParentObject
. This supports any number or depth of objects. So, if a property called GrandChild
of type GrandChildObject
-- which we will assume supports IPropertyNotification
-- was added to the ChildObject
class, then the ParentObject
would receive notifications about its property changes, in addition to the ChildObject
.
Finally, keep in mind that the Child
property of ParentObject
needs to be initialized, as shown in the constructor of the ParentObject
class. If you initialize the underlying field, then the event handlers will not be hooked up.
Complex class solution
Complex classes have both properties and operations, executed through methods. The .NET collection classes are perfect examples of complex classes. They have a single property called Item
-- again, this equates to the "this" accessor in C# -- and have numerous operations that can be applied to this property, such as Add
and Remove
. We will focus on implementing property notification for a generic list class. The same concepts shown here can be applied to any complex class.
There are a couple problems with implementing IPropertyNotification
as-is for collections. First, if any of the objects in the collection support the IPropertyNotification
interface, then their events would not be propagated. Second, the information provided in the property change events do not indicate how the collection changed, e.g. item added, item inserted, item removed.
In order to solve the two problems listed above, we will implement a custom generic List class that supports the IPropertyNotification
interface. Our generic list class will use an instance of System.Collections.Generic.List<T>
internally to store its items. This allows us to focus on adding the functionality we want without having to worry about the details required for a list implementation. We will implement all of the interfaces supported by System.Collections.Generic.List<T>
, but we will integrate calls to the IPropertyNotification
events. The full implementation of our generic list class is too long to be included here, but is included in the demo project. Regardless, we will cover two of the methods here.
Before we dive into that, we are going to create a base class to help us with firing these events from list objects. In addition, we will add two classes derived from PropertyNotificationEventArgs
for our new list. When we fire the IPropertyNotification
events from our list, it would be nice to capture exactly what has changed. For example, if I add an object to the list then I would like the event arguments to include the object and its position in the list.
To that end, our first class will be called ListPropertyNotificationEventArgs
. We will add an enumeration property that describes the list operation that was performed, e.g. Add
, Remove
. We will also add an Int32 property to hold the index of the item. This class and the associated enumeration are shown here:
public enum ListOperation
{
Unknown,
Add,
Remove,
Insert,
Clear,
Set
}
public class ListPropertyNotificationEventArgs
: PropertyNotificationEventArgs
{
#region Constructors
public ListPropertyNotificationEventArgs(String propertyName)
: base(propertyName)
{
this.index = -1;
this.operation = ListOperation.Unknown;
}
public ListPropertyNotificationEventArgs(String propertyName,
Object oldValue, Object newValue)
: base(propertyName, oldValue, newValue)
{
this.index = -1;
this.operation = ListOperation.Unknown;
}
public ListPropertyNotificationEventArgs(String propertyName,
ListOperation operation, Int32 index,
Object oldValue, Object newValue)
: base(propertyName, oldValue, newValue)
{
this.index = index;
this.operation = operation;
}
#endregion
#region Properties/Fields
private Int32 index;
public Int32 Index
{
get
{
return this.index;
}
}
private ListOperation operation;
public ListOperation Operation
{
get
{
return this.operation;
}
}
#endregion
}
The second class derives from CancelPropertyNotificationEventArgs
and adds the same properties as ListPropertyNotificationEventArgs
. Therefore the code for this class, called ListCancelPropertyNotificationEventArgs
, is not included here. I would prefer to have the class named CancelListPropertyNotificationEventArgs
and have it derive from ListPropertyNotificationEventArgs
. Unfortunately, in order to use the new class in the existing PropertyChanging
event it needs to derive from CancelPropertyNotificationEventArgs
.
I could get around this problem by changing the signature of the PropertyChanging
event so that it accepts an EventArgs
class, but I chose not to do that. Another option would be to make an EventArgs
derived class that has two properties: Cancel
and EventArgs
. The former property would indicate whether to cancel the event. The latter would hold another EventArgs
that contains the actual event data. This would also allow us to reuse the PropertyNotificationEventArgs
for both events. However, for now we will ignore both of these ideas and just duplicate the code in ListCancelPropertyNotificationEventArgs
.
Next, we will create a new base class called ListPropertyNotificationObject
, which will derive from PropertyNotificationObject
. Since this class simply adds methods that make firing the IPropertyNotification
events easier, I have not included its code here. Our list class will be called PropertyNotificationList<T>
. It will implement the IList<T>
interface and thus the ICollection<T>
, IEnumerable<T>
and IEnumerable
interfaces. It is defined as shown here:
[Serializable]
public class PropertyNotificationList<T> : ListPropertyNotificationObject,
ICollection<T>, IEnumerable<T>,
IEnumerable, IList<T>
{
}
Now that we have the class definition, we will implement the interfaces and duplicate the List<T>
constructors. First, we will look at the Add
method as shown here:
public void Add(T item)
{
Int32 index = this.items.Count;
if (false == OnPropertyChanging("Item",
ListOperation.Add, index, null, item))
{
this.items.Add(item);
AddPropertyEventHandlers(item);
OnPropertyChanged("Item", ListOperation.Add,
index, null, item);
}
}
As you can see, the Add
method is very similar to the SetProperty
method. We fire the changing event, apply the change and then fire the changed event. One thing to note here is that we add IPropertyNotification
event handlers to the item being added. We do this because we want to know if that object is changed, just like we did earlier with the ChildObject
. Conversely, in the Remove
method, we remove the event handlers from the item as shown here:
public Boolean Remove(T item)
{
Int32 index = IndexOf(item);
if (index >= 0)
{
if (false == OnPropertyChanging("Item",
ListOperation.Remove, index, item, null))
{
RemovePropertyEventHandlers(item);
this.items.RemoveAt(index);
OnPropertyChanged("Item", ListOperation.Remove,
index, item, null);
}
}
return false;
}
The remaining methods in the list are implemented in a similar manner.
Conclusion
Using the new propagation feature, clients can hook up event handlers to a single object to receive property changing and property changed notifications for the given object and any sub-object. Our new generic list class allows clients to receive property changing and changed notifications from a collection. In addition, our new generic list class can propagate any events up to its parent or parents.
Wrap-up
We have extended our property notification infrastructure to include event suppression and event propagation. These two features add finer control over when the events are fired and simplify the use of the events when working with multiple levels of objects. 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 series, we will tackle the following improvements:
- 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 of the properties that changed and how they changed.
History
- 30 May, 2007 -- Original version posted
- 1 August, 2007 -- Article edited and moved to the main CodeProject.com article base