ObservableCollection… better

ObservableCollection class is probably one of the worst classes I have ever seen in the .NET fx:

  • Some current collection methods are missing: (AddRange for example)
  • Bad for performance when we Refresh the collection (1 Clear + n Add) => n+1 events sent to UI to refresh
  • The Clear event “forgets” to give us the removed items even if we have an OldItems property in the event arg.

This class is a spot in our great .NET framework.

I wanted to redefine it but to do it quickly, I only inherited it.

First, the Reset issue.

With WPF or SL, when you use a collection which implements INotifyCollectionChanged , UI automatically adds hander to CollectionChanged event.

So, we will implement explicitly the interface to manage the event ourselves.

    public new event NotifyCollectionChangedEventHandler CollectionChanged;
 
    event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
    {
        add { CollectionChanged += value; }
        remove { CollectionChanged -= value; }
    }

Then, we will add two methods: BeginEdit and EndEdit

    public bool IsEditing { get; private set; }
 
    public void BeginEdit()
    {
        if (! IsEditing)
            IsEditing = true;
   
 
    public void EndEdit()
    {
        if (IsEditing)
        {
            IsEditing = false;
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
   
 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        if (CollectionChanged != null && ! IsEditing)
            CollectionChanged(this, e);
    }  

Now, when we want to refresh the collection, we can call BeginEdit, Clear then the AddRange we will make now and the EndEdit.

    public void AddRange(IEnumerable<T> items)
    {
        bool isEditing = IsEditing;
        IsEditing = true;
        foreach (T item in items)
            Add(item);
        IsEditing = isEditing;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList()));
    }

Finally, we will stop the Clear Reset event in order to send an event with deleted items. However, we want to keep a Reset action (better for UI than a Remove). The problem here is the fact that NotifyCollectionChangedEventArgs doesn’t allow us to set OldItems with Reset Action.

So I will code my own MyNotifyCollectionChangedEventArgs class which inherits NotifyCollectionChangedEventArgs. The issue here is the fact that we can’t change OldItems (no protected set or virtual get). Just a note, its type is IList which is a shame. IEnumerable would be better.

To resolve OldItems issue, we have three possibilities: you accept the risk that NotifyCollectionChangedEventArgs can change and you use reflection to change the private field value (bad):

public class MyNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs
{
    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action)
        : base(action)
    {
   
 
    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList items)
        : base(action, items)
    {
   
 
    public new IList OldItems
    {
        get { return base.OldItems; }
        set
        {
            typeof(NotifyCollectionChangedEventArgs).GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, value);
        }
    }
}

You ask developers to cast the event arg if they want to have the OldItems.

Or you do both but you can guarantee only with the Cast:

public class MyNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs
{
    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action)
        : base(action)
    {
    }
 
    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList items)
        : base(action, items)
    {
        _oldItems = base.OldItems;
   
 
   
private IList _oldItems;
    public new IList OldItems
    {
        get { return _oldItems; }
        set
        {
            _oldItems = value;
            try
            {
                typeof(NotifyCollectionChangedEventArgs).GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, value);
            }
            catch
            {
            }
        }
    }
}

Now, we can override ClearItems method:

    protected override void ClearItems()
    {
        var removedItems = this.ToList();
        bool isEditing = IsEditing;
        IsEditing = true;
        base.ClearItems();
        IsEditing = isEditing;
        OnCollectionChanged(new MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) { OldItems = removedItems });
    }

It’s all folks! Our MyObservableCollection<T> class will be faster and more practicable:

public class MyObservableCollection<T> : ObservableCollection<T>, INotifyCollectionChanged
{
    public MyObservableCollection()
    {
    }
    public MyObservableCollection(IEnumerable<T> items)
        :base(items)
    {
   
 
    public void AddRange(IEnumerable<T> items)
    {
        bool isEditing = IsEditing;
        IsEditing = true;
        foreach (T item in items)
            Add(item);
        IsEditing = isEditing;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList()));
   
 
    protected override void ClearItems()
    {
        var removedItems = this.ToList();
        bool isEditing = IsEditing;
        IsEditing = true;
        base.ClearItems();
        IsEditing = isEditing;
        OnCollectionChanged(new MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) { OldItems = removedItems });
   
 
    public bool IsEditing { get; private set; } 
 
    public void BeginEdit()
    {
        if (! IsEditing)
            IsEditing = true;
   
 
    public void EndEdit()
    {
        if (IsEditing)
        {
            IsEditing = false;
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
   
 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        if (CollectionChanged != null && ! IsEditing)
            CollectionChanged(this, e);
   
 
    public new event NotifyCollectionChangedEventHandler CollectionChanged; 
 
    event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
    {
        add { CollectionChanged += value; }
        remove { CollectionChanged -= value; }
    }
}

This entry was posted in 13461, 7671, 7672, 8708. Bookmark the permalink.

3 Responses to ObservableCollection… better

  1. Tim says:

    Do you have an example of this class being used? I am trying to fill the MyObservableCollection with results from LINQ to SQL. I’d like to see how to pupulate it and then how to refresh it.

  2. Dear Mr. Mezil says:

    I am still climbing the steep WPF learning-curve.
    Your code for the better ObservableCollection is crystal clear and wonderfully helpful not only for what it does, but for its use as a teaching aid.
    Thank you for it.

    I have added a RemoveRange method to my implementation of your code. This is such an obvious addition that I wondered if there was a good reason NOT to have such a method?

    To be honest, I cannot really detect that much improvement in the speed with which your class performs (compared with ObservableCollection, I mean); I expect that is because I have other clumsy coding practices getting in the way of performance.
    But I would prefer to use your class anyway – because it results in much cleaner code.

    Thank you again,

    Noel Macara

  3. bahar says:

    i cant handle the collectionchanged event
    i have a gridview and a radtreeview in a userconrol
    the grid view bind to the observablecollection and treeview use that observablecollection but not binding and i provide it with codebehind
    now i want handle collectionchanged event for relation twoway between grid and tree and i want that if the grid change the tree going to chang too
    plz help
    tnx

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>