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.
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.
If you delete your changes and accept them, you will see that the save notice goes away. Accordingly, if you make changes to both string
s 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 string
s 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.
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.
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:
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()
{
_myType = this.GetType();
}
#region Dirty Status Management
public event PropertyChangedEventHandler DirtyStatusChanged;
private static ConcurrentDictionary<String,
Object> _changes = new ConcurrentDictionary<String, Object>();
public object GetChangedData(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!_changes.ContainsKey(propertyName)) return null;
return _changes[propertyName];
}
public void ClearChangedData()
{
_changes.Clear();
RaiseDataChanged("");
}
public bool HasChangedData
{
get
{
return _changes.Count > 0;
}
}
private void CheckDataChange(string propertyName, Object newPropertyValue)
{
if (string.IsNullOrWhiteSpace(propertyName))
return;
if (_changes.ContainsKey(propertyName))
{
if (_changes[propertyName].Equals(newPropertyValue))
{
object oldValueObject = null;
_changes.TryRemove(propertyName, out oldValueObject);
RaiseDataChanged(propertyName);
}
else
{
}
}
else
{
if (!_changes.TryAdd(propertyName, TestAndCastClassProperty(propertyName)))
throw new ArgumentException("Unable to add
specified property to the changed data dictionary.");
else
RaiseDataChanged(propertyName);
}
}
private void RaiseDataChanged(string propertyName)
{
if (DirtyStatusChanged != null)
DirtyStatusChanged(this, new PropertyChangedEventArgs(propertyName));
RaisePropertyChanged("HasChangedData");
}
private object TestAndCastClassProperty(string Property)
{
if (string.IsNullOrWhiteSpace(Property))
return null;
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
{
IsSomeStringPropertyValid(_someString);
return _someString;
}
set
{
if (_someString != value)
{
CheckDataChange("SomeString", value);
_someString = value;
RaisePropertyChanged("SomeString");
}
}
}
public string SomeOtherString
{
get
{
IsSomeOtherStringPropertyValid(_someOtherString);
return _someOtherString;
}
set
{
if (_someOtherString != value)
{
SetPropertyValue(value, () => _someOtherString = value);
}
}
}
protected void SetPropertyValue(object newValue,
Action setValue, [CallerMemberName] string propertyName = null)
{
CheckDataChange(propertyName, newValue);
setValue();
RaisePropertyChanged(propertyName);
}
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:
_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