Click here to Skip to main content
15,881,898 members
Articles / Desktop Programming / WPF

Introducing the AwesomeObservableCollection

Rate me:
Please Sign up or sign in to vote.
3.67/5 (6 votes)
15 Sep 2014CPOL8 min read 13.5K   11   8
How to extend the default ObservableCollection with some powerful new features

Introduction

Let's start the post by first stating the obvious:

ObservableCollections are awesome!

There, I said it.

Anyone that has done some WPF development will also admit that the use of ObservableCollection in conjunction with the MVVM pattern makes application development, and life, a lot easier.

However, like any control/data structure, you sometimes find it just doesn't provide all the functionality that you need to perform certain tasks.

Now, if you are a regular user of ObservableCollection, or even a non-believer (at the moment), this post will give a brief overview of what the default ObservableCollection offers, and will highlight a couple of issues I have encountered over the years.

Rather than just pointing out the issues, we will also see how to "fix" them, and thereby improve the ObservableCollection for future use.

Ready? Let's go.

Overview of the Default ObservableCollection

So for people that do not know what the ObservableCollection is, here is the MSDN definition:

Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.

I think the definition above is quite clear and concise as to what the ObservableCollection offers. The notifications it refer to work exceptionally well when it comes to MVVM and databinding.

These notifications are triggered via the CollectionChanged event, so whenever the items of the collection change, properties that are bound to the ObservableCollection will be notified, and the UI/whatever can be updated with the new items. Powerful stuff.

It isn't without a few missing features though. In the next section, we will take a quick look at some of the features I think are missing from the default ObservableCollection.

Issues with ObservableCollection

Issue 1: Adding/Removing Multiple Items

Let us take a closer look at that definition again:

Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.

From the definition, is it safe to make the assumption that the change notifications will be triggered in the following pseudo calls:

  • list.Add(someItem)
  • list.Remove(someItem)
  • list.AddRange(someItemList)
  • list.ClearRange()

Got your answers ready? Here goes:

The answers:

  • 1+2: Yes
  • 3+4: No

You may ask why it will not trigger for the last 2 calls, and the answer is simple: ObservableCollection does not provide functionality to add/remove more than one list item at a time.

Your initial reaction might be something like, "who cares?" To show why this might be an issue, picture the following, not totally ridiculous scenario:

Your UI is databound to an ObservableCollection, and it is populated with a couple of items. Now we need to add quite a few, say 50, new items to the list. Easy right? We just use a foreach to traverse over the new items and add them one by one like so:

C#
foreach(var item in itemList)
    list.Add(item)

Yeah, that will work, but it will trigger a change notification each time an item is added to the list. That means your databindings are updated 50 times. This can lead to some serious performance issues with your applications.

Sure, there might be ways around this limitation, like populating a separate non-databound list, and then setting the ObservableCollection to that new list, but that will not work in all scenarios.

We will see how we can get past this limitation without too much trouble.

Issue 2: Only Collection Changes Trigger Notifications

The INotifyPropertyChanged interface is probably the most well known interface when it comes to WPF applications. It enables developers to properly leverage the databinding feature of WPF by automatically triggering change notifications when a property of an object changes.

Let's take a look at a simple example:

C#
public class Person : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get{ return _name;}
        set
        {
            if(_name==value)
                return;
            _name=value;
            NotifyPropertyChanged("Name");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Assuming we have done the UI databinding correctly, whenever the Name property of the Person object changes, it will notify the UI to refresh itself to reflect the new value.

But if we have an ObservableCollection containing Person objects, will it update the UI if a Person object's name changes? To put it bluntly: No.

That is because the ObservableCollection only triggers when the collection itself changes, and it does not monitor the objects it contains for changes.

We will now address the issues pointed out, starting with the easiest one first.

Adding Collection Support

In this section, we will provide the ObservableCollection the ability to add/remove multiple items to/from the collection, without raising notifications for each item.

The simplest way to achieve this is by extending the ObservableCollection.

We will create a new type of ObservableCollection, and simply call it RangeObservableCollection.

The logic of the new class is simple enough:

  • Add 2 new methods, namely AddRange and ClearRange.
  • Override the OnCollectionChanged event, to check if the notification is suppressed before raising the change notifications.
  • In our new methods, we then suppress notifications while we leverage the existing Add and ClearItems methods.
  • Reset the suppression when we are done.
  • Raise the notification ourselves after modifying the collection.

When implemented, the completed class will look as follows:

C#
public class RangeObservableCollection<T> : ObservableCollection<T>
{
    private bool _suppressNotification = false;
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification)
            base.OnCollectionChanged(e);
    }

    public void AddRange(IEnumerable<T> list)
    {
        if (list == null)
            throw new ArgumentNullException("list");

        _suppressNotification = true;

        foreach (T item in list)
            Add(item);

        _suppressNotification = false;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void ClearRange()
    {
        _suppressNotification = true;

        ClearItems();

        _suppressNotification = false;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

Something to note: When we trigger the change notifications after modifying the collection, we have to use the NotifyCollectionChangedAction.Reset value, to indicate that the entire list has changed. The default Add and Remove methods triggers NotifyCollectionChangedAction.Add and NotifyCollectionChangedAction.Remove actions respectively, for each item that is added/removed from the collection. We cannot use these actions because we are now only issuing one change notification, and thus cannot use the actions that are reserved for use on single items.

Not exactly rocket science so far, so let us go onto the next issue.

Adding Object Change Notification

Caveat: The solution we are going to implement will only work if the ObservableCollection contains items that implement the INotifyPropertyChanged interface.

Okay, don't let the abovementioned warning scare you off. In almost all cases where we want the ObservableCollection to notify us of property changes in the items itself, those objects will most likely already be implementing the INotifyPropertyChanged interface.

Okay, let's get to work.

Like the previous section, we will extend the ObservableCollection class by inheriting from it.

Let's create a new class called ItemObservableCollection, and it will inherit from ObservableCollection, same as the RangeObservableCollection.

At the moment, your new class should be something like this:

C#
public class ItemObservableCollection<T> : ObservableCollection<T>
{
}

Since we will only cater to objects that implement the INotifyPropertyChanged interface, we need to limit the generic types that can be stored in the collection, by changing our new class definition as follows:

C#
public class ItemObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
}

The logic for the new class will be:

  • Create new method that will raise change notification when a property on an item changes
  • Change the functionality of the CollectionChanged event, so that we can perform some custom logic
  • Unsubscribe all old items in the collection from property change notifications
  • Subscribe all new items to raise the new property change notifications

That's it. The implemented class should look like this when done:

C#
public class ItemObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    public ItemObservableCollection() 
    {
        this.CollectionChanged += CollectionChanged_Handler;
    }
    void CollectionChanged_Handler(object sender, NotifyCollectionChangedEventArgs e)
    {
        //unsubscribe all old objects
        if (e.OldItems != null)
        {
            foreach (T x in e.OldItems)
                x.PropertyChanged -= ItemChanged;
        }

        //subscribe all new objects
        if (e.NewItems != null)
        {
            foreach (T x in e.NewItems)
                x.PropertyChanged += ItemChanged;
        }
    }

    private void ItemChanged(object sender, PropertyChangedEventArgs e)
    {
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

There we have it. This new class will now raise a notification whenever one of its items has a property that changes.

Nice.

Extending ObservableCollection into Awesomeness

Now you might be starting to ask yourself: where is this AwesomeObservableCollection that was alluded to? Hang on, we are almost there.

We have now seen how easy it is to work around some of the limitations of the default ObservableCollection by simply creating two new classes that provide some great additional functionality.

So what if we want to have an ObservableCollection that deals with both these issues? This is where the AwesomeObservableCollection comes in. It is simply a new class that will have the same functionality as the two previous classes in one place, with a few slight modifications.

When we create the new AwesomeObservableCollection class with the implementations exactly as we did it in the sections above, it should look something like this (separated into regions for clarity):

C#
public class AwesomeObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    public AwesomeObservableCollection()
    {
        base.CollectionChanged += CollectionChanged_Handler;
    }

    #region RangeObservableCollection

    private bool _suppressNotification = false;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification)
            base.OnCollectionChanged(e);
    }

    public void AddRange(IEnumerable<T> list)
    {
        if (list == null)
            throw new ArgumentNullException("list");

        _suppressNotification = true;

        foreach (T item in list)
        {
            Add(item);
        }
        _suppressNotification = false;

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

    public void ClearRange()
    {
        _suppressNotification = true;

        ClearItems();

        _suppressNotification = false;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    #endregion RangeObservableCollection

    #region ItemObservableCollection

    void CollectionChanged_Handler(object sender, NotifyCollectionChangedEventArgs e)
    {
        //unsubscribe all old objects
        if (e.OldItems != null)
        {
            foreach (T x in e.OldItems)
                x.PropertyChanged -= ItemChanged;
        }

        //subscribe all new objects
        if (e.NewItems != null)
        {
            foreach (T x in e.NewItems)
                x.PropertyChanged += ItemChanged;
        }
    }

    private void ItemChanged(object sender, PropertyChangedEventArgs e)
    {
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    #endregion ItemObservableCollection
}

However, we are not done yet. This is where things get a bit tricky. See the caveat mentioned in the RangeObservable section that we have to deal with when adding/removing ranges for a refresher of one more issues we have to deal with.

Since we are suppressing change notifications when adding/clearing ranges, the change notifications won't trigger after each item has been added/removed from the collection with a NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Remove action. It will only trigger the CollectionChanged handler once, with a NotifyCollectionChangedAction.Reset argument, as per our implementation. That means we won't have any e.OldItems or e.NewItems in the CollectionChanged_Handler, so any items added/removed won't be subscribing/unsubscribing to the property change notifications.

We will now need to add the code to handle these situations in our AddRange and ClearRange methods. Modify the methods to look like this:

C#
public void AddRange(IEnumerable<T> list)
{
    if (list == null)
        throw new ArgumentNullException("list");</p>
    _suppressNotification = true;

    foreach (T item in list)
    {
        Add(item);
        item.PropertyChanged += ItemChanged;
    }
    _suppressNotification = false;

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

public void ClearRange()
{
    _suppressNotification = true;
    foreach (T item in Items)
        item.PropertyChanged -= ItemChanged;

    ClearItems();

    _suppressNotification = false;
    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

From the code, you should be able to see that the only change we had to make was to simply subscribed and unsubscribed the items to the change notifications ourselves.

The final implementation of AwesomeObservableCollection is here for the sake of completeness, and because I like you guys.

C#
public class AwesomeObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    public AwesomeObservableCollection()
    {
        base.CollectionChanged += CollectionChanged_Handler;
    }

    #region RangeObservableCollection

    private bool _suppressNotification = false;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification)
            base.OnCollectionChanged(e);
    }

    public void AddRange(IEnumerable<T> list)
    {
        if (list == null)
            throw new ArgumentNullException("list");

        _suppressNotification = true;

        foreach (T item in list)
        {
            Add(item);
            item.PropertyChanged += ItemChanged;
        }
        _suppressNotification = false;

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

    public void ClearRange()
    {
        _suppressNotification = true;

        foreach (T item in Items)
            item.PropertyChanged -= ItemChanged;

        ClearItems();

        _suppressNotification = false;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    #endregion RangeObservableCollection

    #region ItemObservableCollection

    void CollectionChanged_Handler(object sender, NotifyCollectionChangedEventArgs e)
    {
        //unsubscribe all old objects
        if (e.OldItems != null)
        {
            foreach (T x in e.OldItems)
                x.PropertyChanged -= ItemChanged;
        }

        //subscribe all new objects
        if (e.NewItems != null)
        {
            foreach (T x in e.NewItems)
                x.PropertyChanged += ItemChanged;
        }
    }

    private void ItemChanged(object sender, PropertyChangedEventArgs e)
    {
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    #endregion ItemObservableCollection
}

Conclusion

In this post, we have gone through a couple of scenarios that the default ObservableCollection does not cope with very well, and looked at ways we can deal with it. In the process, we have created 3 fully functional types of ObservableCollections that can be used depending on you specific scenarios.

Hope you enjoyed it, and learned something in the process.

Please remember to vote, and feel free to comment with feedback/suggestions/compliments.

Thanks for reading.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Inivit Systems
South Africa South Africa
I am currently employed by Inivit Systems. Here we strive to use cutting edge technologies, in order to provide our clients with the best possible software and continue to offer them a competitive edge.

We are responsible for all kinds of applications, ranging from web applications to desktop applications, including integration into some older legacy systems.

I love programming and solving puzzles, but beside that I enjoy experimenting with new technologies, reading, watching movies and relaxing with friends.

Comments and Discussions

 
Questionvery helpfull Pin
MemoryBug4-Dec-14 23:33
MemoryBug4-Dec-14 23:33 
Questionno cigar Pin
jfriedman15-Sep-14 11:28
jfriedman15-Sep-14 11:28 
This could be improved by providing all entries that changed at once via the event or their id's.

Suppressing the event is a paradigm one will only find useful if they adopted these semantics which are mediocre.

Right idea wrong implementation.

Try again for sure!
General[My vote of 1] Some more (not so awesome lol) comments... Pin
SledgeHammer0115-Sep-14 10:27
SledgeHammer0115-Sep-14 10:27 
GeneralRe: [My vote of 1] Some more (not so awesome lol) comments... Pin
Pete O'Hanlon15-Sep-14 23:36
mvePete O'Hanlon15-Sep-14 23:36 
GeneralRe: [My vote of 1] Some more (not so awesome lol) comments... Pin
SledgeHammer0116-Sep-14 4:59
SledgeHammer0116-Sep-14 4:59 
GeneralRe: [My vote of 1] Some more (not so awesome lol) comments... Pin
Pete O'Hanlon16-Sep-14 5:02
mvePete O'Hanlon16-Sep-14 5:02 
QuestionSome comments Pin
William E. Kempf15-Sep-14 6:45
William E. Kempf15-Sep-14 6:45 
AnswerRe: Some comments Pin
Pete O'Hanlon15-Sep-14 10:00
mvePete O'Hanlon15-Sep-14 10:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.