I have come to love MVVM, but every now and then WPF comes back and reminds us that it often wants to be used like Windows Forms with its event-based code-behind model. Nowhere is this more pronounced than in the WPF DataGrid -which comes with all sorts of helpful plumbing for filtering, sorting, and tracking dirty states. Unfortunately a lot of this functionality is tied to events on the control, which means that at some point you’ll either need to put View-related code into the ViewModel or else use code-behind. I hit one such problem today where I wanted to know if an ObservableCollection, that I was using as an ItemSource for a DataGrid, was dirty or not. By ‘dirty’ I mean that either the contents of the list, or any of the objects in the list, had changed. However if all changed properties were returned to their original state then I would consider that the data was no longer dirty.
Essentially what I wanted to implement was a ‘trackable’ observable collection similar to Ludwig’s one here. Unfortunately the requirement to ‘un-dirty’ the collection if data returns to its original state, makes it a bit trickier. Now this is where using WCF, even for non-distributed systems, can really pay off. There are two reasons for this:
- SvcUtil can be used to automatically implement INotifyPropertyChanged on every public property of a data contract (i.e. model class) when creating proxy classes. This means that with automated proxy rebuilds (see this post) you get property notifications guaranteed on all data contracts with no extra effort.
- WCF ‘model’ objects in SvcUtil generated proxy classes are deep-cloneable by default. This is because all ‘model’ objects are DataContracts and all fields are DataMembers (otherwise they wouldn’t be picked up from the WSDL by SvcUtil in the first place), so serializing and then de-serializing an object will give you an exact copy that isn’t the same object. This is a technique used in IDesign’s SerializationUtil helper class.
The net effect is that we have an easy way of being notified when a property has changed, but also of checking whether the object (i.e. data contract) as a whole has changed. So, taking ‘inspiration’ from Ludwig’s example (i.e. trapping the property change notification per-object), we can include something that also compares objects every time a property on the object changes – but only if the object was in the original collection (because additions/inserts to the list are dirty-by-default). For this we can use the aforementioned IDesign helper classes, to which we can add a few extra methods for comparing objects. Rather than attempt to compare byte or ASCII streams, we can compute an MD5 hash of the object and compare that instead.
My solution is simply a leaner modification of the TrackableObservableCollection<T> class. Internally it wraps every ‘original’ object in the list in a TrackableObject<T> class, which handles the plumbing of notifying the parent list of changes via the OnDirtyStatusChanged event. The TrackableObservableCollection<T> inherits from ObservableCollection<T> which saves us having to recode a whole raft of plumbing.
Despite what you may think, given the constant serialization/deserialization, the object comparison is very quick (longest time taken is 6ms). What matters though is that the solution performs well enough. I’m sure that someone clever has a more efficient way of doing this, but what I like is that it’s all made so easy and plumbing-light by the use of WCF.
To use, simply pass your existing ObservableCollection to the constructor, then bind to the DataGrid:
// get the data ObservableCollection<TestClass> myCollection = GetData(); // convert to a trackable collection TrackableObservableCollection<TestClass> myTrackableCollection = new TrackableObservableCollection<TestClass>(myCollection); Debug.Assert(!myTrackableCollection.IsDirty); // store a value DateTime? originalValue = myCollection.DateOfBirth; // change the value myCollection.DateOfBirth = DateTime.Now; Debug.Assert(myTrackableCollection.IsDirty); // change the value back myCollection.DateOfBirth = originalValue; Debug.Assert(!myTrackableCollection.IsDirty);
Rather than detail every single aspect of the solution, it’s easier just to download the (VS2010) example.
Points to note:
- For the sake of simplicity I’ve used ‘Add Service Reference’ rather than SvcUtil in the build events.
- Be careful when using EF-derived data contracts with circular navigation properties as updating a value may not apply the update to the respective navigation property (a problem that can be averted by not using lazy loading needlessly)