Click here to Skip to main content
Click here to Skip to main content

Almost-automatic INotifyPropertyChanged, automatic IsDirty, and automatic ChangeTracking

By , 26 Aug 2009
 

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)

About the Author

Sandrino Di Mattia
Technical Lead RealDolmen
Belgium Belgium
Member
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

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionWrap a POCO insteadmemberAmazingFactory10 May '13 - 2:06 
GeneralMy vote of 1memberMember 466870517 Nov '09 - 9:45 
QuestionPost Sharp?memberWillemToerien26 Aug '09 - 22:36 
AnswerRe: Post Sharp? [modified]memberEdgar Frank31 Aug '09 - 21:20 
GeneralRe: Post Sharp?memberSandrino Di Mattia1 Sep '09 - 7:10 
GeneralRe: Post Sharp?memberEdgar Frank1 Sep '09 - 22:07 
GeneralRe: Post Sharp?memberSandrino Di Mattia2 Sep '09 - 4:00 
GeneralGeneric base classmemberSteve Hansen26 Aug '09 - 22:07 
GeneralRe: Generic base class [modified]memberSandrino Di Mattia1 Sep '09 - 7:12 
GeneralRe: Generic base classmemberSteve Hansen1 Sep '09 - 7:17 
GeneralAOPmemberseesharper26 Aug '09 - 21:02 
GeneralAnti-patternsmemberDmitri Nesteruk26 Aug '09 - 19:20 
GeneralRe: Anti-patternsmemberSandrino Di Mattia1 Sep '09 - 7:19 

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 26 Aug 2009
Article Copyright 2009 by Sandrino Di Mattia
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid