INotifyPropertyChanging and INotifyPropertyChanged in Real Life using Expression Trees
Functional Requirements:
Requirements when we need to achieve any or all of the functionalities listed below
- Undo/Redo functionality in an Application
- Any of user interface patterns e.g. MVC, MVP, MVVM etc
- Other patterns State Pattern, Events, Event handlers etc.
In this blog I’ll discuss the points mentioned below
- Sample Scenario
- High level Solution
- Solution using .Net Framework 2 (and Drawbacks)
- Solution using .Net Framework 3.5 using Lambda Expressions
- Summary
Sample Scenario:
We need to implement Undo/Redo feature in our application. I have a Barometer class that records pressure. We need to implement achieve Undo/Redo feature for this sample class.
public class Barometer
{
private double pressure;
public double Pressure
{
get
{
return pressure;
}
set
{
pressure = value;
}
}
}
High Level Solution:
I can implement an Undo stack and Redo stack. Whenever the pressure will change I will maintain the state in the Undo/Redo stacks. For every Undoable/Redoable action there will be an entry in these stack. For this simple scenario one can assume a stack to be implemented as Stack<KeyValuePair<string, string>>, where key will correspond to action and value for the property value in KeyValuePair. On Undoing and Redoing the action I’ll load the state from the stacks and apply on the Barometer object using Stack’s Push and Pop operations.
In order to implement that I will be implementing interfaces listed below
- INotifyPropertyChanging
- INotifyPropertyChanged
e.g. Pressure has to be updated from “0” to “60”. In this case we can save “0” as it is the previous state in the Undo stack. Redo stack will be empty as there is no action to Redo. The sequence of events for “Pressure” property will be [Before Update<”0”>] –> Write value to Undo Stack –> [Perform Update<”60”>]
Now if I have to to Undo the action that I performed I’ll copy the state from the Undo stack. Then write the state to the Redo stack so that I can Redo the action and then apply the state on the “Pressure” property.
I can use an Command pattern here and if an command implements IUndoable and IRedoable behavior we can have this framework in place.
In this blog I won’t be going into the details of Command pattern and Undo Redo implementation. I can extend this blog if the need arises as the main focus here is to discuss the high level implementation. I’ll focus on the interfaces INotifyPropertyChanging and INotifyPropertyChanged and their usage in this scenario.
Solution using .Net framework 2.0:
To implement this prior to .Net Framework 2.0 the general way is displayed in code snippet below
public class Barometer : INotifyPropertyChanged, INotifyPropertyChanging
{
private double pressure;
public double Pressure
{
get
{
return pressure;
}
set
{
if (value != pressure)
{
//Maintaining the temporary copy of event to avoid race condition
PropertyChangingEventHandler propertyChanging = PropertyChanging;
if (propertyChanging != null)
{
propertyChanging(this, new PropertyChangingEventArgs("Pressure"));
}
pressure = value;
//Maintaining the temporary copy of event to avoid race condition
PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs("Pressure"));
}
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region INotifyPropertyChanging Members
public event PropertyChangingEventHandler PropertyChanging;
#endregion
}
In the code snippet above I am maintaining the temporary copy to avoid possibility of a race condition in case the last subscriber unsubscribe after the null check and before the event is raised.
As displayed in Red text we are hard coding the name of the property. Here we have only one property but in real world we had to do the same across all the properties for which we needed to have the same behavior.
The issues with this approach are
Cumbersome as you have to do the same across the application
- Prone to typing mistakes
- Any refactoring will break the code
We can achieve the same without these drawbacks in .Net framework 3.5 and above using Lambda expressions.
Solution using .Net framework 3.5:
The Barometer class updated to use Lambda Expression is displayed in code snippet below
public class Barometer : INotifyPropertyChanged, INotifyPropertyChanging
{
private double pressure;
public double Pressure
{
get
{
return pressure;
}
set
{
//Using Lambda Expressions, Anonymous methods and Extension methods
PropertyChanged.SetPropertyValue(PropertyChanging, value, () => this.Pressure, new Action<double>(delegate(double newValue) { pressure = newValue; }));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region INotifyPropertyChanging Members
public event PropertyChangingEventHandler PropertyChanging;
#endregion
}
The difference with the code snippet in .Net 2.0 version is in the set block in code snippet displayed above.
Using the Lambda expressions i.e. (()=>this.Pressure) along with anonymous methods and Extension methods we can achieve the same in much better way.
I have created one more class in which the Extension method SetPropertyValue is defined. This is a generic class and the functionality of method is explained in the inline comments. This is displayed in code snippet below
public static class PropertyChangeExtensions
{
public static T SetPropertyValue<T>(this PropertyChangedEventHandler postHandler,
PropertyChangingEventHandler preHandler, T newValue, Expression<Func<T>> oldValueExpression,
Action<T> setter)
{
//Retrieve the old value
Func<T> getter = oldValueExpression.Compile();
T oldValue = getter();
//In case new and old value both are equal to default
//values for that type or are same return
if ((Equals(oldValue, default(T)) && Equals(newValue, default(T)))
|| Equals(oldValue, newValue))
{
return newValue;
}
//Retrieve the property that has changed
var body = oldValueExpression.Body as System.Linq.Expressions.MemberExpression;
var propInfo = body.Member as PropertyInfo;
string propName = body.Member.Name;
var targetExpression = body.Expression as ConstantExpression;
object target = targetExpression.Value;
//Maintaining the temporary copy of event to avoid race condition
var preHandlerTemp = preHandler;
//Raise the event before property is changed
if (preHandlerTemp != null)
{
preHandlerTemp(target, new PropertyChangingEventArgs(propName));
}
//Update the property value
setter(newValue);
//Maintaining the temporary copy of event to avoid race condition
var postHandlerTemp = postHandler;
//Raise the event after property is changed
if (postHandlerTemp != null)
{
postHandlerTemp(target, new PropertyChangedEventArgs(propName));
}
return newValue;
}
}
Summary:
Thanks for your patience to read up to this. This can be optimized by avoiding the repeated resolution of the expression tree(happening for every property change event). Implementing some sort of caching strategy will do the trick. Well is this the best solution. I’ll say “Depends”.
Pros:
- Code should be easily refactored
- Performance issues if any in this approach are considered to be insignificant
- Solution can be extendable by using interfaces like ISerializable and saving state in Undo/Redo stacks for only the event i.e. chunk and not the complete state
- You can control the properties for which you want to implement this kind of behavior by applying custom attributes
Cons:
- Incase of a object tree in which you are having circular references this approach may or may not work. All depends how the application is designed
For a complex object tree I think one approach is to start with the parent object and then iterate over the child nodes until you reach the leafs. For every command that can be Undoable/Redoable you need to have and unique ID (GUID) and then you can save the state of the application against that when performing push/pop operation to Stack.
References:
More Effective C#: 50 Specific Ways to Improve Your C#
Technorati Tags: .Net Framework 3.5,Lambda Expressions,Expression Trees,INotifyPropertyChanging,INotifyPropertyChanged,Anonymous Methods,Extension Methods