Click here to Skip to main content
Licence Ms-PL
First Posted 26 Aug 2009
Views 18,665
Downloads 137
Bookmarked 27 times

Almost-automatic INotifyPropertyChanged, automatic IsDirty, and automatic ChangeTracking

By | 26 Aug 2009 | Article
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)

About the Author

Sandrino Di Mattia

Software Developer
RealDolmen
Belgium Belgium

Member

Follow on Twitter Follow on Twitter
I'm a .NET Software Engineer at RealDolmen, one of the largest players on the Belgian IT market: http://www.realdolmen.com
 

All posts also appear on my blog: http://blog.sandrinodimattia.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

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralMy vote of 1 PinmemberMember 46687059:45 17 Nov '09  
QuestionPost Sharp? PinmemberWillemToerien22:36 26 Aug '09  
AnswerRe: Post Sharp? [modified] PinmemberEdgar Frank21:20 31 Aug '09  
GeneralRe: Post Sharp? PinmemberSandrino Di Mattia7:10 1 Sep '09  
GeneralRe: Post Sharp? PinmemberEdgar Frank22:07 1 Sep '09  
GeneralRe: Post Sharp? PinmemberSandrino Di Mattia4:00 2 Sep '09  
GeneralGeneric base class PinmemberSteve Hansen22:07 26 Aug '09  
GeneralRe: Generic base class [modified] PinmemberSandrino Di Mattia7:12 1 Sep '09  
GeneralRe: Generic base class PinmemberSteve Hansen7:17 1 Sep '09  
GeneralAOP Pinmemberseesharper21:02 26 Aug '09  
GeneralAnti-patterns PinmemberDmitri Nesteruk19:20 26 Aug '09  
GeneralRe: Anti-patterns PinmemberSandrino Di Mattia7:19 1 Sep '09  

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.

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