Almost-automatic INotifyPropertyChanged, automatic IsDirty, and automatic ChangeTracking






4.42/5 (12 votes)
A refactor friendly implementation of INotifyProperty changed, without using Reflection.
Introduction
WPF… it looks like I need to build workarounds for everything I’m trying to do. Prior to WPF, I rarely needed INotifyPropertyChanged
.
But since we need it, why not make the best of it. Since I want a base class for all my future objects, in all my future projects I want something super clean.
- Implement
INotifyPropertyChanged
in the best way possible (refactor friendly and optimal performance) - Automatically set the
IsDirty
flag - Automatic change tracking to be able to log what has been changed (like in SharePoint when you get alerts, I love that!)
- The change tracking should also be exported as text so I can write it to SQL
Of course, I don’t have to tell you that the code must always ensure a minimal amount of work. And, it should be refactor friendly etc… I want perfect code.
The code
/// <summary>
/// This object automatically implements notify property changed
/// </summary>
public class NotifyPropertyChangeObject : INotifyPropertyChanged
{
/// <summary>
/// Track changes or not.
/// If we're working with DTOs and we fill up the DTO
/// in the DAL we should not be tracking changes.
/// </summary>
private bool trackChanges = false;
/// <summary>
/// Changes to the object
/// </summary>
public Dictionary<string, object> Changes { get; private set; }
/// <summary>
/// Is the object dirty or not?
/// </summary>
public bool IsDirty
{
get { return Changes.Count > 0; }
set { ; }
}
/// <summary>
/// Event required for INotifyPropertyChanged
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// This constructor will initialize the change tracking
/// </summary>
public NotifyPropertyChangeObject()
{
// Change tracking default
trackChanges = true;
// New change tracking dictionary
Changes = new Dictionary<string, object>();
}
/// <summary>
/// Reset the object to non-dirty
/// </summary>
public void Reset()
{
Changes.Clear();
}
/// <summary>
/// Start tracking changes
/// </summary>
public void StartTracking()
{
trackChanges = true;
}
/// <summary>
/// Stop tracking changes
/// </summary>
public void StopTracking()
{
trackChanges = false;
}
/// <summary>
/// Change the property if required and throw event
/// </summary>
/// <param name="variable"></param>
/// <param name="property"></param>
/// <param name="value"></param>
public void ApplyPropertyChange<T, F>(ref F field,
Expression<Func<T, object>> property, F value)
{
// Only do this if the value changes
if (field == null || !field.Equals(value))
{
// Get the property
var propertyExpression = GetMemberExpression(property);
if (propertyExpression == null)
throw new InvalidOperationException("You must specify a property");
// Property name
string propertyName = propertyExpression.Member.Name;
// Set the value
field = value;
// If change tracking is enabled, we can track the changes...
if (trackChanges)
{
// Change tracking
Changes[propertyName] = value;
// Notify change
NotifyPropertyChanged(propertyName);
}
}
}
/// <summary>
/// Get member expression
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression"></param>
/// <returns></returns>
public MemberExpression GetMemberExpression<T>(Expression<Func<T,
object>> expression)
{
// Default expression
MemberExpression memberExpression = null;
// Convert
if (expression.Body.NodeType == ExpressionType.Convert)
{
var body = (UnaryExpression)expression.Body;
memberExpression = body.Operand as MemberExpression;
}
// Member access
else if (expression.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpression = expression.Body as MemberExpression;
}
// Not a member access
if (memberExpression == null)
throw new ArgumentException("Not a member access",
"expression");
// Return the member expression
return memberExpression;
}
/// <summary>
/// The property has changed
/// </summary>
/// <param name="propertyName"></param>
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Convert the changes to an XML string
/// </summary>
/// <returns></returns>
public string ChangesToXml()
{
// Prepare base objects
XDeclaration declaration = new XDeclaration("1.0",
Encoding.UTF8.HeaderName, String.Empty);
XElement root = new XElement("Changes");
// Create document
XDocument document = new XDocument(declaration, root);
// Add changes to the document
// TODO: If it's an object, maybe do some other things
foreach (KeyValuePair<string, object> change in Changes)
root.Add(new XElement(change.Key, change.Value));
// Get the XML
return document.Document.ToString();
}
}
Using the code
public class Person : NotifyPropertyChangeObject
{
private string firstName;
private string lastName;
private int age;
/// <summary>
/// FirstName
/// </summary>
[DefaultValue("")]
public string FirstName
{
get { return firstName; }
set { ApplyPropertyChange<Person,
string>(ref firstName, o => o.FirstName, value); }
}
/// <summary>
/// LastName
/// </summary>
[DefaultValue("")]
public string LastName
{
get { return lastName; }
set { ApplyPropertyChange<Person,
string>(ref lastName, o => o.LastName, value); }
}
/// <summary>
/// Age
/// </summary>
[DefaultValue(0)]
public int Age
{
get { return age; }
set { ApplyPropertyChange<Person, int>(ref age, o => o.Age, value); }
}
}
That’s it…
A little word about the code
- Our class
Person
inherits fromNotifyPropertyChangeObject
. - When we change the value of a property, the magic happens.
- We forward the private variable, the (refactor friendly) property, and the new value.
- After that, we check if the ‘new’ value matches the old one. If this is the case, nothing should happen.
- If the values do not match, we want to change the value and notify that a change has happened.
- In the meanwhile, we also add that change to the Changes dictionary.
- It’s safe to presume that if the dictionary contains changes, the object is dirty.
Finally, there are the methods Reset
, StartTracking
, and StopTracking
. These have to do with the change tracking.
If I’m filling up a DTO in my DAL, I don’t want it to be marked as dirty. So before I start, I call StopTracking
, and when I’m done, I call StartTracking
.
Later on, if I save my object, I want it to be clean (not dirty), so I call the Reset
method.
Finally, you could call ChangesToXml
to get a string representation of all the changes. This could the be written to SQL or so...
Example application
At the top of the article, you can download a working project. I’ve also added an example for the cases where you cannot inherit from NotifyPropertyChangeObject
, when you’re working with Entity Framework or LINQ to SQL for example…