Home > .NET General, Silverlight > Change tracking in object graph

Change tracking in object graph

Download Source Code from here

On this project I was working on, I had to monitor objects and object graphs for changes so I could display this information to the user. Having looked around the internet for solutions, I could not find any single solution that I felt suited my needs perfectly, so I decided to take a stab at the problem myself.

The requirements for my change tracker was the following:

  • Very easy to use, preferably a single line of code to enable and disable change tracking for an object and its entire object graph
  • The solution had to support an unknown object graph depth and automatically adapt to  changes in the graph depth (attach to new objects added and detach from objects removed, including items added to and removed from collections)
  • Automatically listen for changes to all properties of objects, but support adding exceptions for properties that should not be tracked (if any)
  • Support explicitly defining which properties that should be change tracked
  • Except for implementing the INotifyPropertyChanged and INotifyCollectionChanged interfaces, there should be no other requirements imposed on the objects for enabling change tracking
  • Change notifications should be easy to subscribe to and contain data about the changes that had been made

To jump to the conclusion, what I ended up with was the following, which attaches to an item and starts listening for changes to the item and its entire object graph:

var observer = ObjectGraphObserver.AttachObserver(this.Item1, 
                (s, e) => DumpChangeTrackingInfo(s, e));

//....and detach when the time comes
ObjectGraphObserver.DetachObserver(observer);

 

The first line of code creates an instance of the ObjectGraphObserver which starts listening for changes made to an object (“Item1”) and its object graph by passing in the object root as the first argument. The second argument passed to the method is the callback that should be called when a change occurs. In this case I call a method which dumps information about the change that occurs, but you could also do all other sorts of things like (conditionally) raising a dirty flag to indicate to the user that a change has occurred.

The DumpChangeTrackingInfo method looks like this. You can see I’m using the data passed along from the observer to provide some “useful” information to the user.

private void DumpChangeTrackingInfo(object sender, ObservedPropertyChangedEventArgs e)
{
  if (e is ObjectChangedEventArgs)
  {
      ObjectChangedEventArgs oce = (ObjectChangedEventArgs)e;
      
      StringBuilder sb = new StringBuilder(this._changeTrackingData);
      sb.AppendLine();
      sb.AppendFormat("Property name = '{0}', Sender name = '{2}'", 
          oce.PropertyName, oce.Sender, oce.SenderName);

      this.ChangeTrackingData = sb.ToString();

  }
  else if (e is CollectionChangedEventArgs)
  {
      CollectionChangedEventArgs cce = (CollectionChangedEventArgs)e;
      
      StringBuilder sb = new StringBuilder(this._changeTrackingData);
      sb.AppendLine();
      sb.AppendFormat("Property name = '{0}', Sender name = '{2}', Action = '{3}'", 
          cce.PropertyName, cce.Sender, cce.SenderName, cce.Action);

      if (cce.Action == NotifyCollectionChangedAction.Add)
      {
          for (int i = 0; i < cce.NewItems.Count; i++)
          {
              Level1Item lvlItem = (Level1Item)cce.NewItems[i];
              sb.AppendLine();
              sb.AppendFormat("\t Name = '{0}'", lvlItem.Name);
          }
      }

      this.ChangeTrackingData = sb.ToString();                
  }
}

The AttachObserver and DetachObserver methods looks like this:

public static ObjectGraphObserver AttachObserver(object target, 
	Action<object, ObservedPropertyChangedEventArgs> handler)
{
  ObjectGraphObserver observer = new ObjectGraphObserver(target);
  observer.ChangeDetected += new EventHandler<ObservedPropertyChangedEventArgs>(handler);
  return observer;
}

public static void DetachObserver(ObjectGraphObserver observer)
{
  if (observer != null)
  {
      observer.Detach();
      observer._changeDetectedHandler = null;
      observer = null;
  }
}

When an ObjectGraphObserver is created by passing the root object to the constructor, reflection is used to walk the object graph and setting up the change monitoring. By using reflection, you all know that this solution will not be extremely fast for setting up the change tracking for very large object graphs, but for my needs it was more than fast enough. I usually make sure to create the observers on a separate thread to not block the UI or other processes. The ObjectGraphObserver also has constructor overloads that lets you specify a max depth at which the observer stops tracking changes.

The ObjectGraphObserver passes two kinds of event args to the callback when a change occurs; the ObjectChangedEventArgs for changes to properties of an object, and the CollectionChangedEventArgs when a collection changes (items being added or removed). Both event args contains information about the object that was changed and which property of the object that was affected. The CollectionChangedEventArgs also contains information about the item that was added to or removed from the collection.

To prevent an object in the object graph that implements INotifyPropertyChanged or INotifyCollectionChanged from being change tracked, you can decorate a property with the NonChangeTrackableAttribute attribute. This will make the ObjectGraphObserver ignore that property when it walks the object graph. All properties of a type that implements INotifyPropertyChanged / INotifyCollectionChanged not tagged by the NonChangeTrackableAttribute attribute will be tracked (unless an explicit list of types and properties are specified, which I’ll demonstrate shortly).

The code snippet below shows an example of using the NonChangeTrackableAttribute attribute.

public class Level2ItemWithNonChangeTrackableProp : INPC
{
   private string _name;
   public string Name
   {
       get { return _name; }
       set { SetProperty(ref this._name, value, () => this.Name); }
   }

   // Updating the Child properties will not trigger a change notification 
   // through the ObjectGraphObserver 
   private Level2Item _child;
   [NonChangeTrackable]  
   public Level2Item Child
   {
       get { return _child; }
       set { SetProperty(ref this._child, value, () => this.Child); }
   }
}

The above example shows how to set up exceptions for properties that should NOT be tracked. I also needed to support the opposite approach; explicitly specifying only the types and properties that should be tracked. Using this approach, only the types and properties explicitly defined in a mapping list are being tracked, all the other types and properties are being ignored.

The following example shows how to create an observer which listens for and notifies about changes made to the “Name” and “Count” properties of “Level2Item” instances. I’ve also set the max depth to 10, meaning the observer will not monitor objects deeper than 10 levels from the root object.

 

var observer = ObjectGraphObserver.AttachObserver(this.Item3, 
                GetItem3Properties(), 10, 
                (s, e) => DumpChangeTrackingInfo(s, e));

private Dictionary<Type, List<string>> GetItem3Properties()
{
  Dictionary<Type, List<string>> propMap = new Dictionary<Type, List<string>>();
  propMap.Add(typeof(Level2Item), new List<string> { "Name", "Count" });
  return propMap;
}

I’m not saying this is an ideal solution to the problem, but it works for good for me. There are issues regarding circular references that I do not handle. Also, the fact that I’m using reflection to set things up makes it “slow”, so if you’re planning on using the same approach, be aware of the implications of attaching to a large object (graph).

Categories: .NET General, Silverlight Tags:
  1. No comments yet.
  1. No trackbacks yet.
You must be logged in to post a comment.