Click here to Skip to main content
15,887,027 members
Articles / Desktop Programming / WPF

Almost-automatic INotifyPropertyChanged, automatic IsDirty, and automatic ChangeTracking

Rate me:
Please Sign up or sign in to vote.
4.42/5 (13 votes)
26 Aug 2009Ms-PL2 min read 70.5K   796   32   14
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

C#
/// <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

C#
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)


Written By
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

Comments and Discussions

 
Generalload This approach is not working for a list of 1 million objects with tracking enabled. And this approach doesn't support concurrent change of same property. Pin
Schizofreindly8110-Jun-15 5:07
Schizofreindly8110-Jun-15 5:07 
QuestionWrap a POCO instead Pin
AmazingFactory10-May-13 2:06
AmazingFactory10-May-13 2:06 
GeneralMy vote of 1 Pin
Member 466870517-Nov-09 9:45
Member 466870517-Nov-09 9:45 
QuestionPost Sharp? Pin
WillemToerien26-Aug-09 22:36
WillemToerien26-Aug-09 22:36 
AnswerRe: Post Sharp? [modified] Pin
Edgar Frank31-Aug-09 21:20
Edgar Frank31-Aug-09 21:20 
GeneralRe: Post Sharp? Pin
Sandrino Di Mattia1-Sep-09 7:10
Sandrino Di Mattia1-Sep-09 7:10 
GeneralRe: Post Sharp? Pin
Edgar Frank1-Sep-09 22:07
Edgar Frank1-Sep-09 22:07 
GeneralRe: Post Sharp? Pin
Sandrino Di Mattia2-Sep-09 4:00
Sandrino Di Mattia2-Sep-09 4:00 
GeneralGeneric base class Pin
Steve Hansen26-Aug-09 22:07
Steve Hansen26-Aug-09 22:07 
GeneralRe: Generic base class [modified] Pin
Sandrino Di Mattia1-Sep-09 7:12
Sandrino Di Mattia1-Sep-09 7:12 
GeneralRe: Generic base class Pin
Steve Hansen1-Sep-09 7:17
Steve Hansen1-Sep-09 7:17 
GeneralAOP Pin
seesharper26-Aug-09 21:02
seesharper26-Aug-09 21:02 
I would have to agree with Dmitri on this one.

I am faced with the exact same problem that I need to implement INotifyPropertyChanged in order to support change tracking and two way databinding.

I take a different approach where I inject the INotifyPropertyChanged implementation at runtime.

I use Mono.Cecil to weave the assembly (implement INotifyPropertyChanged) before it gets loaded into the app domain.

As for the state tracking I have a state tracker object where objects implementing INotifyPropertyChanged can be registered.

Regards

Bernhard Richter
GeneralAnti-patterns Pin
Dmitri Nеstеruk26-Aug-09 19:20
Dmitri Nеstеruk26-Aug-09 19:20 
GeneralRe: Anti-patterns Pin
Sandrino Di Mattia1-Sep-09 7:19
Sandrino Di Mattia1-Sep-09 7:19 

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

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