Click here to Skip to main content
Click here to Skip to main content
Go to top

Almost-automatic INotifyPropertyChanged, automatic IsDirty, and automatic ChangeTracking

, 26 Aug 2009
Rate this:
Please Sign up or sign in to vote.
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 from NotifyPropertyChangeObject.
  • 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…

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Sandrino Di Mattia
Technical Lead RealDolmen
Belgium Belgium
I'm a Technical Consultant at RealDolmen, one of the largest players on the Belgian IT market: http://www.realdolmen.com
 
All posts also appear on my blogs: http://blog.sandrinodimattia.net and http://blog.fabriccontroller.net
Follow on   Twitter

Comments and Discussions

 
QuestionWrap a POCO instead PinmemberAmazingFactory10-May-13 2:06 
GeneralMy vote of 1 PinmemberMember 466870517-Nov-09 9:45 
QuestionPost Sharp? PinmemberWillemToerien26-Aug-09 22:36 
AnswerRe: Post Sharp? [modified] PinmemberEdgar Frank31-Aug-09 21:20 
GeneralRe: Post Sharp? PinmemberSandrino Di Mattia1-Sep-09 7:10 
GeneralRe: Post Sharp? PinmemberEdgar Frank1-Sep-09 22:07 
GeneralRe: Post Sharp? PinmemberSandrino Di Mattia2-Sep-09 4:00 
GeneralGeneric base class PinmemberSteve Hansen26-Aug-09 22:07 
GeneralRe: Generic base class [modified] PinmemberSandrino Di Mattia1-Sep-09 7:12 
GeneralRe: Generic base class PinmemberSteve Hansen1-Sep-09 7:17 
GeneralAOP Pinmemberseesharper26-Aug-09 21:02 
GeneralAnti-patterns PinmemberDmitri Nesteruk26-Aug-09 19:20 
GeneralRe: Anti-patterns PinmemberSandrino Di Mattia1-Sep-09 7:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140926.1 | Last Updated 26 Aug 2009
Article Copyright 2009 by Sandrino Di Mattia
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid