Background

In order to postpone notification we have to temporarily reroute them to a holding place and fire them all once delay is no longer required. At the same time we need to continue to provide normal behavior and notifications for other consumers of the collection which do not require delay.

This could be achieved if we have multiple objects acting like a shell and manipulating the same collection. One instance will contain the element’s container and be a host for all of the notification events which consumers will be subscribed to and other instances of the shell will handle disabled and delayed events. These extra shells reference the same container but instead of firing events which consumer handlers attached to they will call its own handlers which either collect these events or discard them.

ObservableCollection is based on a Collection class which implements functionality and ObservableCollection implements notifications. Collection class is implemented as a shell around IList interface. It contains a reference to container which exposes IList interface and manipulates this container through it. One of the constructors of Collection class takes IList as a parameter and allows this list to be a container for that Collection. This creates a way to have a multiple Collection instances to manipulate the same container which perfectly serves our purpose.

Unfortunately this ability is lost in ObservableCollection implementation. Instead of assigning IList to be a container for the instance it creates a copy of that List and uses that copy to store elements. This limitation prevents us from inheriting from ObservableCollection class.

ObservanleCollectionEx is based on a Collection class as well and implements exactly the same methods and properties that ObservableCollection does. In addition to these members ObservanleCollectionEx exposes two methods to create disabled or delayed notification shells around the container. Methods of the shell created by DisableNotifications() produce no notifications on neither INotifyPropertyChanged nor INotifyCollectionChanged. Calls to the methods of shell created by DelayNorifications() produce no notifications until this instance goes out of scope or IDisposable.Dispose() has been called on it.

How it works

Except for a few performance twicks ObservableCollectionEx behaves exactly as ObservableCollection. It uses Collection to perform its operations, notifies consumers via INotifyPropertyChanged and INotifyCollectionChanged and creates a copy of the List if you pass it to a constructor.

The differences start when DelayNotifications() or DisableNotifications () methods are called. This method creates a new instance of the ObservableCollectionEx object and passes its constructor a reference to the original ObservableCollectionEx object and a Boolean which specifies if notifications are disabled or postponed. This new instance will have same interface as the original, same element’s container but none of the consumer handlers attached to the CollectionChanged event. So when methods of this instance are called and events are fired none of these are going anywhere but to temporary storage.

Once updates are done and either this instance goes out of scope or Dispose() has been called all of the collected events are fired on CollectionChanged and PropertyChanged of the original object notifying all of the consumers about changes.

Using the code

ObservableCollectionEx should be used exactly as ObservableCollection. It could be instantiated and used in place of ObservableCollection or it could be derived from. No special treatment is required.

In order to postpone notifications it is recommended to use using() directive:

ObservableCollectionEx target = new ObservableCollectionEx();
using (ObservableCollectionEx iDelayed = target.DelayNotifications())
{
       iDelayed.Add(item0);
       iDelayed.Add(item0);
       iDelayed.Add(item0);
}
 
  
Due to design of notification arguments it is not possible to combine different operations together. For example it is not possible to Add and Remove elements on the same delayed instance unless Dispose() has been called in between these calls. Calling Dispose() will fire previously collected events and reinitialize operation.
ObservableCollectionEx<T> target = new ObservableCollectionEx<T>(); 
using (ObservableCollectionEx<T> iDelayed = target.DelayNotifications())
{     
    iDelayed.Add(item0); 
    iDelayed.Add(item0); 
}

using (ObservableCollectionEx<T> iDelayed = target.DelayNotifications())
{
   iDelayed.Remove(item0);
   iDelayed.Remove(item0);
}


using (ObservableCollectionEx<T> iDelayed = target.DelayNotifications())             
{     
   iDelayed.Add(item0); 
   iDelayed.Add(item0); 

   iDelayed.Dispose();

   iDelayed.Remove(item0);
   iDelayed.Remove(item0);
}

 
Performance

In general both ObservableCollection and ObservableCollectionEx provide comparable performance. Testing has been done using array of 10,000 unique objects. Both ObservableCollection and ObservableCollectionEx where initialized with this array to pre allocate storage so it is not affecting timing results. Application has been run about dozen times to let JIT to optimize executable before test results were collected.

The test consisted of 10,000 Add, Replace, and Remove operations. Timing has been collected using Stopwatch class and presented in milliseconds.

The value on the left represents number of milliseconds it took to complete the test (Add, Replace, and Remove). Value on the bottom specifies number of notification subscribers (Handlers added to CollectionChanged event).

As you can see from the graph performance of interface with disabled notifications does not depend on subscribers. Due to several performance enhancements ObservableCollectionEx performs slightly better than ObservableCollection regardless of number of subscribers but it is obviously loses to Disabled interface once there are more than one subscriber.

Last edited Sep 6, 2011 at 7:40 PM by ENikS, version 21

Comments

No comments yet.