Posts
56
Comments
307
Trackbacks
0
January 2010 Entries
Make KeyedCollection<TKey, TItem> to work properly with WPF data binding

In my previous post, I went through creating the KeyedCollectionEx class which allows easier consumption of the KeyedCollection class (no need to derive anymore, just provide a delegate).  One of the problem I encountered was that when using it as an ItemsSource in WPF, any changes to the collection will not be shown in the UI.  This is because the class doesn’t implement INotifyCollectionChanged interface.  So, let’s add that.

The implementation is fairly straightforward; the class itself has all of the methods that will actually change the collection (SetItem, InsertItem, ClearItems and RemoveItem) to be virtual so we just need to override it.  With the implementation, the code looks like the following (only listing the INotifyCollectionChanged implementation, see previous post for the constructor & delegate implementation):

public class KeyedCollectionEx<TKey, TItem> : KeyedCollection<TKey, TItem>, INotifyCollectionChanged
{
   // Overrides a lot of methods that can cause collection change
   protected override void SetItem(int index, TItem item)
   {
      base.SetItem(index, item);
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, index)); 
   }

   protected override void InsertItem(int index, TItem item)
   {
      base.InsertItem(index, item);
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); 
   }

   protected override void ClearItems()
   {
      base.ClearItems();
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
   }

   protected override void RemoveItem(int index)
   {
      TItem item = this[index];
      base.RemoveItem(index);
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item));
   }

   protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
   {
      if (CollectionChanged != null)
         CollectionChanged(this, e);
   }

   #region INotifyCollectionChanged Members

   public event NotifyCollectionChangedEventHandler CollectionChanged;

   #endregion
}

With the above code, now using this collection as an ItemsSource will cause WPF UIElements to show newly added / removed entities properly.  However, as detailed in my other post way back, if the collection is used a lot and does get bound as ItemSource to a lot of UIElements, adding / removing multiple items can hamper performance.

To alleviate that issue I’m adding a public method called AddRange (mimicking the AddRange method of List<T>).  I need a boolean variable (which I named _deferNotifyCollectionChanged) to indicate if the class should raise the event or not – if I don’t do that each call to Add will result in a CollectionChanged event being raised.  The code will then look like as follows:

private bool _deferNotifyCollectionChanged = false;
public void AddRange(IEnumerable<TItem> items)
{
   _deferNotifyCollectionChanged = true;
   foreach (var item in items)
      Add(item);
   _deferNotifyCollectionChanged = false;
   OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<TItem>(items)));
}

protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
   if (_deferNotifyCollectionChanged)
      return;

   if (CollectionChanged != null)
      CollectionChanged(this, e);
}

There’s a problem with this though; if you call this method a NotSupportedException exception will be thrown; the message says ‘Range actions are not supported’.  Essentially the UIElements doesn’t support change notification where multiple items are changed.  As such we have to provide a workaround for this; either using the approach detailed in my other post, or by changing the AddRange method to the following:

public void AddRange(IEnumerable<TItem> items)
{
   _deferNotifyCollectionChanged = true;
   foreach (var item in items)
      Add(item);
   _deferNotifyCollectionChanged = false;

   OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

The code above is simpler than the other post (well, I’m learning new tips and tricks over the years and months).  Instead of specifying what has been added, the NotifyCollectionChangedAction used is Reset, which signals the CollectionChanged event subscribers that the collection has changed tremendously so it should re-read the whole collection.  Please use with care; if you have a list which have 10000 items and you’re only adding a small amount to that list, a Reset will be much more expensive than notifying individual addition.

Below is pasted the full source of the class.  Since the behavior has changed, I’m also renaming the class to ObservableKeyedCollection, which sounds better than adding an Ex to an existing class name.  I hope this can be of use to others.

public class ObservableKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>, INotifyCollectionChanged
{
   private Func<TItem, TKey> _getKeyForItemDelegate;

   // Constructor now requires a delegate to get the key from the item
   public ObservableKeyedCollection(Func<TItem, TKey> getKeyForItemDelegate) : base()
   {
      if (getKeyForItemDelegate == null)
         throw new ArgumentNullException("Delegate passed can't be null!");

      _getKeyForItemDelegate = getKeyForItemDelegate;
   }

   protected override TKey GetKeyForItem(TItem item)
   {
      return _getKeyForItemDelegate(item);
   }

   // Overrides a lot of methods that can cause collection change
   protected override void SetItem(int index, TItem item)
   {
      base.SetItem(index, item);
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, index)); 
   }

   protected override void InsertItem(int index, TItem item)
   {
      base.InsertItem(index, item);
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); 
   }

   protected override void ClearItems()
   {
      base.ClearItems();
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
   }

   protected override void RemoveItem(int index)
   {
      TItem item = this[index];
      base.RemoveItem(index);
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item));
   }

   private bool _deferNotifyCollectionChanged = false;
   public void AddRange(IEnumerable<TItem> items)
   {
      _deferNotifyCollectionChanged = true;
      foreach (var item in items)
         Add(item);
      _deferNotifyCollectionChanged = false;

      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
   }

   protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
   {
      if (_deferNotifyCollectionChanged)
         return;

      if (CollectionChanged != null)
         CollectionChanged(this, e);
   }

   #region INotifyCollectionChanged Members

   public event NotifyCollectionChangedEventHandler CollectionChanged;

   #endregion
}
  • Share This Post:
  • Share on Twitter
  • Share on Facebook
  • Share on Technorati
Posted On Tuesday, January 12, 2010 3:42 AM | Feedback (1)
Using KeyedCollection<TKey, TItem>

A little over 2 years ago, I saw my peer’s code that uses the KeyedCollection<TKey, TItem> class.  I never seen it before, and it is actually a pretty nice class.  It is essentially a dictionary class, but with the stipulation that the key for each item added to the dictionary can be retrieved from the item itself. 

With a Dictionary, every time we wanted to add a new item into it, we have to call its Add method, which accepts 2 parameters, the key for the item and the item itself.  In most cases, the key already is in the item, so we’re just reduplicating it.  This KeyedCollection class provides a shortcut since we can just provide it a method so it can get the key for each item added automatically.

The following code show how Dictionary and KeyedCollection can be used:

// Class to contain data stuff
public class MyData
{
   public int Id { get; set; }
   public string Data { get; set; }
 }

// KeyedCollection is an abstract class, so have to derive
public class MyDataKeyedCollection : KeyedCollection<int, MyData>
{
   protected override int GetKeyForItem(MyData item)
   {
      return item.Id;
   }
}

private void Test()
{
   MyData temp, md = new MyData() { Id = 1, Data = "Test" };
   
   // Using a Dictionary
   Dictionary<int, MyData> dict = new Dictionary<int, MyData>();
   dict.Add(md.Id, md);  // Add to dictionary
   temp = dict[1];       // Retrieve

   // Using KeyedCollection
   KeyedCollection<int, MyData> keyd = new MyDataKeyedCollection();
   keyd.Add(md);         // Add to KeyedCollection
   temp = keyd[1];       // Retrieve
}

As you can see, usage is very, very similar to a Dictionary, with the only difference that the Add method only accepts 1 parameter.  Retrieval is exactly the same.  However, since KeyedCollection is an abstract class, you cannot use it directly; you have to derive from it and use your derived class – that’s why I created the MyDataKeyedCollection class.  This MyDataKeyedCollection class has to override the GetKeyForItem method (which is declared as an abstract method in the base class) so you can retrieve the key for the given item.  For me this is somewhat of a deal breaker – I have to always derive from it – there’s no way to use it easily, like a typical collection class.

So my next step is to try to make it easier to consume… thus I created my KeyedCollectionEx class.

public class KeyedCollectionEx<TKey, TItem> : KeyedCollection<TKey, TItem>
{
   private Func<TItem, TKey> _getKeyForItemDelegate;
   public KeyedCollectionEx(Func<TItem, TKey> getKeyForItemDelegate) : base()
   {
      if (getKeyForItemDelegate == null)
         throw new ArgumentNullException("Delegate passed can't be null!");

      _getKeyForItemDelegate = getKeyForItemDelegate;
   }

   protected override TKey GetKeyForItem(TItem item)
   {
      return _getKeyForItemDelegate(item);
   }
}

The constructor now requires a delegate that will get the TKey for the given TItem – I’m using the Func delegate which is in .NET 3.0, so if you’re using .NET 2.0, you need to create your own delegate signature.  With this, my test method becomes as follows:

private void Test2()
{
   MyData temp, md = new MyData() { Id = 1, Data = "Test" };

   // Using KeyedCollectionEx with anonymous delegate
   KeyedCollection<int, MyData> keyd = new KeyedCollectionEx<int, MyData>(delegate(MyData myData) { return myData.Id; });
   keyd.Add(md);         // Add to KeyedCollection
   temp = keyd[1];       // Retrieve

   // Using KeyedCollectionEx with lambda expression
   KeyedCollection<int, MyData> keyd2 = new KeyedCollectionEx<int, MyData>(myData => myData.Id);
   keyd2.Add(md);         // Add to KeyedCollection
   temp = keyd2[1];       // Retrieve
}

I demonstrated how to use it with anonymous methods (so I don’t have to create a method for it), and also how to use it with a lambda expression.  Much simpler, IMHO – no need to create a derived class.  However, I would like this to work with WPF’s data binding – so I’d like further enhance my KeyedCollectionEx class so it can notify WPF when items are being added or removed from it – that’ll be my next post.

  • Share This Post:
  • Share on Twitter
  • Share on Facebook
  • Share on Technorati
Posted On Thursday, January 07, 2010 10:42 AM | Feedback (9)