Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Let's Get Dirty!

Rate me:
Please Sign up or sign in to vote.
4.74/5 (20 votes)
17 Dec 2013CPOL11 min read 50.1K   928   32   21
Creating slightly-smarter dirty flags in our model classes

Introduction

Almost anyone that has written a data class or a model class, sooner or later, has needed to add a "dirty flag" to that class to let another part of the application know that the data your class is holding has changed. There are lots of ways to do this but they usually boil down to detecting that a value has changed and setting an internal Boolean to true to tell us that the data is now dirty and needs to be saved. While simple and effective, this mousetrap approach leaves something to be desired. Specifically, if the user reverts their changes, we need to determine if this means the flag should be cleared or not. This complicates the code so we usually just take the easy route and continue to tell the user they need to save their data even when they clearly do not. This results in our apps providing a poor user experience. We need to stop doing that!

What I'll present here is a slightly more intelligent dirty flag... a more modern one if you will... that not only tracks whether a value has changed or not, but will also clear itself if the value is returned to the original value, even if it has changed a number of times in between.

So download the code and settle-in to read on how to improve the lowly dirty flag in your data model classes.

Background

The attached project demonstrates the technique I'll describe here and while it is written in C# and WPF using a basic MVVM structure, it is certainly adaptable to any language that supports similar constructs and by no means restricted to MVVM. (Pro tip: Data binding works in XAML even without MVVM!)

This pattern came about because I was working on a Silverlight project and I needed to track not only the validity of a property value but also whether the user changed the value or not. In testing, it became apparent that it was pretty silly that if the user changed a value, then changed it back to the original value, we were still prompting the user to save the changes. The mousetrap approach to a dirty flag was resulting in a bad user experience. As a developer who cares more about how my users experience our software as opposed to what is quicker and easier for me, it was clear I needed to do something.

While pondering on the problem, I noticed that a solution of sorts already existed via the INotifyDataErrorInfo interface in the System.ComponentModel namespace. The standard data error handling methods already knew how to add and subtract errors and warnings based on the value of a property. So I looked at that for inspiration as to how I could implement similar functionality for value changes.

The Sample

The demo project attached to the article contains a simple implementation of both INotifyDataErrorInfo and my new INotifyDirtyData interface. My sample follows MVVM so there is a view (View/MainWindow.xaml), there is a view model (ViewModel/MainWindowViewModel.cs), and there is a model (Model/DirtyDataModel.cs). There is also an interface to define our dirty data properties, events, and methods (Interfaces/INotifyDirtyData.cs).

The main window is super-simple and contains two text boxes bound to a pair of properties on the data model class.

Image 1

If you change one of the strings and press Enter or Tab to accept the change into the text box, you will see a notice that you have changed the values and you must save your changes.

Image 2

If you delete your changes and accept them, you will see that the save notice goes away. Accordingly, if you make changes to both strings and revert one of them, the notice remains showing that the model knows one of the values is still dirty.

A bonus is the data validation. Deleting one of the strings will not only show the save notice but will also highlight the text box in red to show that you are not meeting the data requirements.

Image 3

The Code

So let's look at the code. Our smarter dirty flag begins with the INotifyDirtyData interface. It defines the event we will raise, the methods we will support, and the property where we will indicate whether any of the monitored properties have changed.

C#
/// <summary>
/// Interface for a data class which implements a smarter dirty flag
/// </summary>
public interface INotifyDirtyData
{
    event PropertyChangedEventHandler DirtyStatusChanged;
    Object GetChangedData(string propertyName);
    void ClearChangedData();
    bool HasChangedData { get; }
} 

When we create a data model, we will inherit this interface. Here is the data model in our sample with the irrelevant parts removed:

C#
internal class DirtyDataModel : INotifyDirtyData, INotifyDataErrorInfo, INotifyPropertyChanged
{
    private string _someString = "Some String";
    private string _someOtherString = "Some Other String";
    private Type _myType;

    public event PropertyChangedEventHandler PropertyChanged;

    public DirtyDataModel()
    {
        // Get the type information for this class and stash it away
        // because we will need it when we get property info.
        _myType = this.GetType();
    }

    #region Dirty Status Management

    // DirtyStatusChanged is the event to notify subscribers 
    // that a specific property is now dirty. We're using the
    // PropertyChangedEventHandler class as a convenient way to pass the property name to a subscriber.
    public event PropertyChangedEventHandler DirtyStatusChanged;

    // changes is our internal dictionary 
    // which holds the changed properties and their original values.
    private static ConcurrentDictionary<String, 
    Object> _changes = new ConcurrentDictionary<String, Object>();

    /// <summary>
    /// Returns the original value of the property so it can be compared to the current
    /// value or used to restore the original value
    /// </summary>
    /// <param name="propertyName">The name of the class property 
    /// to fetch the original value for.</param>
    /// <returns>If an original value is present, that value will be returned. 
    /// If the original value is not present,
    /// the method will return null.</returns>
    public object GetChangedData(string propertyName)
    {
        if (String.IsNullOrEmpty(propertyName) ||
            !_changes.ContainsKey(propertyName)) return null;
        return _changes[propertyName];
    }

    /// <summary>
    /// Clears the record of changed properties and their original values.
    /// </summary>
    /// <remarks>Call this method when the data in the model is saved.</remarks>
    public void ClearChangedData()
    {
        _changes.Clear();
        // Raise the change events to notify subscribers the dirty status has changed
        RaiseDataChanged("");
    }

    /// <summary>
    /// Returns true if one or more monitored properties has changed.
    /// </summary>
    public bool HasChangedData
    {
        get
        {
            return _changes.Count > 0;
        }
    }

    // CheckDataChange should be called in property setters BEFORE the property value is set. It will
    // check to see if it already has a memory of the properties original value. If not, it will inspect
    // the property to get the original value and then save that back raising the DirtyStatusChanged event
    // in the process. If the new value is the same as the original value, the property will be removed from
    // the list of dirty properties.
    private void CheckDataChange(string propertyName, Object newPropertyValue)
    {
        // If we were passed an empty property name, eject.
        if (string.IsNullOrWhiteSpace(propertyName))
            return;

        // Check to see if the property already exists in the dictionary...
        if (_changes.ContainsKey(propertyName))
        {
            // Already exists in the change collection
            if (_changes[propertyName].Equals(newPropertyValue))
            {
                // The old value and the new value match
                object oldValueObject = null;
                _changes.TryRemove(propertyName, out oldValueObject);
                RaiseDataChanged(propertyName);
            }
            else
            {
                // New value is different than the original value...
                // Don't do anything because we already know this value changed.
            }
        }
        else
        {
            // Key is not in the dictionary. Get the original value and save it back
            if (!_changes.TryAdd(propertyName, TestAndCastClassProperty(propertyName)))
                throw new ArgumentException("Unable to add 
                specified property to the changed data dictionary.");
            else
                RaiseDataChanged(propertyName);
        }
    }

    // Raises the events to notify interested parties that one or more monitored properties are now dirty
    private void RaiseDataChanged(string propertyName)
    {
        // Raise the DirtyStatusChanged event passing the name of the changed property
        if (DirtyStatusChanged != null)
            DirtyStatusChanged(this, new PropertyChangedEventArgs(propertyName));

        // Raise property changed on HasChangedData in case something is bound to that property
        RaisePropertyChanged("HasChangedData");
    }

    // Internal method which will get the value of the specified property
    private object TestAndCastClassProperty(string Property)
    {
        if (string.IsNullOrWhiteSpace(Property))
            return null;
        // _myType is the type info for this class and is fetched during construction.
        PropertyInfo propInfo = _myType.GetProperty(Property);
        if (propInfo == null) { return null; }
        return propInfo.GetValue(this, null);
    }

    #endregion Dirty Status Management

    #region Properties & Property Notification

    public string SomeString
    {
        get
        {
            // Check to see if the value for the property is valid before returning it
            IsSomeStringPropertyValid(_someString);
            return _someString;
        }
        set
        {
            if (_someString != value)
            {
                // Check if the new property value makes this property dirty.
                // MUST be called before the internal value of the property is set!
                CheckDataChange("SomeString", value);
                _someString = value;
                // If there is support for CallerMemberName this should pass nothing:
                // RaisePropertyChanged();
                // rather than the explicit property name as demonstrated here:
                RaisePropertyChanged("SomeString");
            }
        }
    }

    public string SomeOtherString
    {
        get
        {
            // Check to see if the value for the property is valid before returning it
            IsSomeOtherStringPropertyValid(_someOtherString);
            return _someOtherString;
        }
        set
        {
            if (_someOtherString != value)
            {
                // This is the lambda version of the property setter and is the preferred method to
                // check for data changes and to set the property values. However,
                // it requires support for the CallerMemberName attribute which isn't available in
                // Portable Class Libraries and maybe other parts of the framework.
                SetPropertyValue(value, () => _someOtherString = value);

                //If there is no support for CallerMemberName, 
                //you would call it adding the property name to the end:
                //SetPropertyValue(value, () => _someOtherString = value, "SomeOtherString");
            }
        }
    }

    protected void SetPropertyValue(object newValue, 
    	Action setValue, [CallerMemberName] string propertyName = null)
    {
        // This is a general way of checking and setting properties which can be called via a lambda.
        CheckDataChange(propertyName, newValue);
        setValue();
        RaisePropertyChanged(propertyName);
    }

    // Standard property change notification
    // NOTICE: The CallerMemberName attribute is not available 
    // in Portable Class Libraries unless you add it yourself!
    protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
    {
        var handler = this.PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion Properties & Property Notification
}

You can see that our class is going to implement INotifyDirtyData (which we have defined), INotifyDataErrorInfo, and INotifyPropertyChanged. The code illustrates an implementation of the INotifyDirtyData interface. You don't technically need to do it this way and for other languages and situations, you may need to change it.

The key here is the _changes dictionary. This dictionary keeps entries for each of the properties that have changed along with their original property values. This is used whenever CheckDataChange is called to determine if we have saved out an original value. The presence of an original value in this dictionary is the flag saying that the value has changed because the only way for the entry to be present is for the value to be different.

Fun tip: At the suggestion of reader TechJosh, I'm using a ConcurrentDictionary. This makes the property setters thread-safe for multi-threaded applications.

Changes are detected by the CheckDataChange method. It must be called in the setter of properties you wish to be monitored for changes. Don't miss that... you don't have to monitor every property. Even if you expose a property on a class which supports this interface, if it doesn't make sense to track whether that property value has changed or not, then don't call the method in the property setter.

The CheckDataChange method has a very important behavior that you must be aware of. It fetches the original value of a property by calling the property getter when it determines it needs to save the value back. This has a profound impact on how you call that method. You must call it before setting the internal value of the property. Typically, a property setter has the line:

C#
_someObject = value;

You must call CheckDataChange right before that line so the method has a chance to get the value of the property before it has been set to the new value. If you call the method after the value is already set, value change detection will not work. (There is a helper method called TestAndCastClassProperty which is responsible for locating and fetching the value of the property before it changes.)

The sample class shows this order of operations in two ways. The SomeString property illustrates a standard property setter where we call everything in order. The SomeOtherString propery illustrates setting the property via a lambda expression. This simplifies the property setter and ensures everything is always called in order. I illustrate both methods because not everybody is comfortable with lambdas. In addition, that calling method, as written, relies on the CallerMemberName attribute which isn't (currently) available in Portable Class Libraries and maybe other parts of the framework. Read the code comments for a full rundown of what is going on there.

When checking if the new value is different from the old value, we look to see if the property is present in the _changes dictionary. If it isn't, we know that we have a new value because we only call CheckDataChange when the new property value is different than the old property value. So we fetch the original value and sock it away in the dictionary raising the DirtyStatusChanged event and raising property change notification on the HasChangedData property.

The View Model would typically hook the DirtyStatusChanged event as a means of managing commands or other logic associated with dirty status in the model. For example, the event handler in the View Model may inspect HasChangedData and enable a "Save" ICommand in response to that value.

The View may bind to HasChangedData to know when to show the user that they must save the data. In my implementation, I raise NotifyPropertyChanged on HasChangedData whenever we raise DirtyStatusChanged. I could have done an internal setter and achieved the same result. Binding to this property allows a value converter to change visibility of an element (like I did in the example) or whatever else you may choose to do.

Saving and Loading Data

It is pretty likely that you are going to need to save or load the data model with data to or from persistent storage, a web service, or some other source sooner or later. One of the side-effects of this implementation is that we don't have a good way of knowing when one of these events takes place. As the developer, you have to plumb this part up.

If you are loading data into the model, you have a couple of choices. If the load method is part of the model (meaning the model is totally self-contained), then you could load the data into the members by using the internal property variables. The downside is that you will not fire the PropertyChangedNotification event for properties where the values are set. If those notifications are required, you might need to manually fire them after loading the data.

Alternately, you could setup an internal flag and simply abort CheckDataChanged when the flag is set. You could even make this a property on the model allowing you to set it from an external class. This could be handy if the model is only a model with no data management functionality (because it is handled in another class).

Finally, the interface defines a ClearChangedData method. Calling this method should clear the dictionary of all entries and is designed to tell the change tracking that the data has been "saved" making the current values the official values. This method could also be called immediately after loading data into the model via the property setters to clear the changes that would result from a data load. This method also raises the change notification events so that subscribers will know that the status of the dirty flag has changed.

Last Bits

The interface also defines a method called GetChangedData(propertyName). Calling this method with a property name will return the original value of that property if it is present in the dictionary. This could be used to determine if a specific property has changed (the return value will be non-null) or it could be used to get back to the original value. That becomes very powerful as a means of undoing changes on the UI regardless of how many changes the user has made. A scenario may be that when a user changes a value, a glyph appears showing that value has changed. If the user wishes, clicking the glyph would restore the original value of that property regardless of how many changes the user made between when the glyph appeared and when the user clicks it.

Alternately, you could walk the class properties when the data is being saved and store out the original values using this method if a property returns a non-null result. This would give you a snapshot of before and after values... essentially a property "diff" that could be useful in your application.

If you wanted or needed deeper change tracking, change the Object in the _changes dictionary to List<Object> and adjust the method to add changes to the list. The order here would be important as reverting to an "older" entry would need to unwind newer entries in the list as well. Remember to remove the property entry from the _changes dictionary if all items in the sub-list are removed. The presence of an entry indicates the property has changed and if there are no entries in the sub-list, you should not have a node in the top dictionary. If this is the kind of functionality you need, I'll leave you to your own devices to come up with the details of a solution for this.

Conclusion

So I hope you find this useful and start including it in your model classes. It implements a simple yet pretty smart way of tracking value changes on properties and provides a way to intelligently inform the user they need to save their work.

I think back on the number of projects I've done over the years and I wish I had come up with this a long time ago!

History

  • Dec 2013 - Initial revision
  • Dec 17, 2013 - Added a call to raise the change events when the ClearChangeData method is called to ensure subscribers are aware of the change in the dirty flag status
  • Jun 2015 - Amended code to incorporate suggestions from TechJosh making it thread safe and adding a few performance improvements

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) NFocus Consulting Inc
United States United States
I'm a Senior Software Developer with King Memory LLC in Columbus, OH working primarily in C# writing APIs and other line of business applications. I've been working in .Net since version 1.0 and I am skilled in most aspects of the platform. I've started out as an Automation Engineer working with robots, lasers, machine vision, and other cool stuff which I miss. I've also worked in healthcare and digital marketing. I speak fluent binary.

On the side I own Aerial Break Software under which I publish personal projects and hope to make some money... someday.

My fun job is shooting Fireworks professionally as an Ohio licensed fireworks exhibitor. I'm licensed for outdoor and indoor fireworks and I've been on the crew for Red, White, and Boom in Columbus, OH since 2002.

Comments and Discussions

 
QuestionQuestion on TestAndCastClassProperty Pin
Joe Pizzi29-Sep-17 18:13
Joe Pizzi29-Sep-17 18:13 
QuestionUnsaved Changes Indicator Pin
Josh161126-Apr-17 3:33
Josh161126-Apr-17 3:33 
AnswerRe: Unsaved Changes Indicator Pin
Jason Gleim5-Jun-17 8:19
professionalJason Gleim5-Jun-17 8:19 
GeneralMy vote of 5 Pin
prashita gupta2-Mar-16 21:09
prashita gupta2-Mar-16 21:09 
GeneralMy vote of 5 Pin
bhalaniabhishek1-Nov-15 4:55
bhalaniabhishek1-Nov-15 4:55 
BugDirtyStatusChanged is always null Pin
TurgayTürk27-Oct-15 0:38
TurgayTürk27-Oct-15 0:38 
GeneralRe: DirtyStatusChanged is always null Pin
Jason Gleim27-Oct-15 7:23
professionalJason Gleim27-Oct-15 7:23 
QuestionRe: DirtyStatusChanged is always null Pin
TurgayTürk27-Oct-15 11:34
TurgayTürk27-Oct-15 11:34 
QuestionGreat article, any idea of how to use with AutoMapper? Pin
Alex Talazar26-Jun-15 3:47
professionalAlex Talazar26-Jun-15 3:47 
AnswerRe: Great article, any idea of how to use with AutoMapper? Pin
Jason Gleim26-Jun-15 4:52
professionalJason Gleim26-Jun-15 4:52 
SuggestionImprovements Pin
TechJosh16-Jun-15 10:38
TechJosh16-Jun-15 10:38 
GeneralRe: Improvements Pin
Jason Gleim17-Jun-15 5:05
professionalJason Gleim17-Jun-15 5:05 
BugString.Empty value Pin
Guybou11-Aug-14 5:30
Guybou11-Aug-14 5:30 
GeneralRe: String.Empty value Pin
Jason Gleim11-Aug-14 5:41
professionalJason Gleim11-Aug-14 5:41 
QuestionMy vote of 5 Pin
Wayne Gaylard12-Dec-13 23:49
professionalWayne Gaylard12-Dec-13 23:49 
QuestionHave you tried this with a web project? Pin
JV999912-Dec-13 20:09
professionalJV999912-Dec-13 20:09 
AnswerRe: Have you tried this with a web project? Pin
Sacha Barber12-Dec-13 23:45
Sacha Barber12-Dec-13 23:45 
GeneralRe: Have you tried this with a web project? Pin
JV999912-Dec-13 23:53
professionalJV999912-Dec-13 23:53 
GeneralRe: Have you tried this with a web project? Pin
Red Feet17-Dec-13 0:26
Red Feet17-Dec-13 0:26 
GeneralRe: Have you tried this with a web project? Pin
Jason Gleim17-Dec-13 3:53
professionalJason Gleim17-Dec-13 3:53 
GeneralRe: Have you tried this with a web project? Pin
Sacha Barber17-Dec-13 5:45
Sacha Barber17-Dec-13 5:45 

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.