Geeks With Blogs
New Things I Learned

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
}
Posted on Tuesday, January 12, 2010 3:42 AM | Back to top


Comments on this post: Make KeyedCollection<TKey, TItem> to work properly with WPF data binding

# re: Make KeyedCollection<TKey, TItem> to work properly with WPF data binding
Requesting Gravatar...
Brilliant, thankyou very much for posting this.
Left by Justin on Nov 22, 2011 11:56 AM

# re: Make KeyedCollection<TKey, TItem> to work properly with WPF data binding
Requesting Gravatar...
Hi and thanks for the code.
Would you also need to have INotifyPropertyChanged implemented for the count property?
Left by Vitaliy on Mar 02, 2013 6:43 AM

# re: Make KeyedCollection<TKey, TItem> to work properly with WPF data binding
Requesting Gravatar...
Great article. Explained very well! Thanks!
Left by James on Jan 03, 2014 9:24 PM

# re: Make KeyedCollection<TKey, TItem> to work properly with WPF data binding
Requesting Gravatar...
Since the [] operator is used by the KeyCollection to looks up the specific item with the give key (i.e item[key]). In the overrided RemoveItem function, TItem item = this[index] should actually be TItem item = this.ElementAt(index) else you are assuming your key is the same as your index
Left by James on Jan 03, 2014 10:00 PM

# re: Make KeyedCollection<TKey, TItem> to work properly with WPF data binding
Requesting Gravatar...
The code for SetItem() throws an ArgumentException because that constructor doesn't support Replace. I think it should be this instead:

protected override void SetItem(int index, TItem item)
{
TItem oldItem = this[index];
base.SetItem(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem, index));
}
Left by Zach on Aug 18, 2015 6:42 AM

# re: Make KeyedCollection<TKey, TItem> to work properly with WPF data binding
Requesting Gravatar...
Should put a try at AddRange to avoid a headache

try
{
_deferNotifyCollectionChanged = true;
foreach (var item in items)
Add(item);
}
finally { _deferNotifyCollectionChanged = false; }
Left by LazyLeecher on Mar 13, 2016 1:21 PM

Your comment:
 (will show your gravatar)


Copyright © Muljadi Budiman | Powered by: GeeksWithBlogs.net