Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / WPF

ObservableCollection Simply Explained

Rate me:
Please Sign up or sign in to vote.
4.75/5 (37 votes)
27 Jun 2015CPOL7 min read 144.1K   1.4K   25   48
Explaining what MSDN does not, and providing an easier way to use ObservableCollection.

Introduction

ObservableCollection is a commonly used class by WPF and other code. The concept is simple enough - creating and using ObservableCollection is the same as a standard Collection.

If however you need to subscribe to the change event of the ObservableCollection things are complicated by both a lack of, but also counterproductive documentation on MSDN and other sources. The task itself is simple - once it is clear what is going on. This article will explain this simple feature, and demonstrate a technique to make it so easy as to not even need much if any documentation.

ObservableCollection

ObservableCollection is a collection which allows subscribers to be notified when the contents of the collection are altered. This includes replacement of objects, deletion, addition, and movements. This is useful in a variety of scenarios but usually used to update user interfaces without having to bind to the user of the collection directly. Instead the subscriber listens to the collection for updates providing for better abstraction and isolation.

CollectionChanged Event

ObservableCollection contains an event named CollectionChanged. The CollectionChanged even notifies subscribers of changes to the collection. This seems simple enough, and in fact should be but is not quite. This is because all notifications are jammed into one event, and the documentation for it on MSDN is both nearly empty and even confusing.

CollectionChanged passes 2 arguments: Sender and e. Sender is the ObservableCollection that is firing the event. e contains several arguments which detail the changes. Specifically they are:

Image 1

MSDN Issues

The issues on MSDN are too numerous and not worthy to detail each and every one. To clarify and correct some important ones, I will highlight a few.

ObservableCollection<T>.CollectionChanged Event

ObservableCollection<T>.CollectionChanged Event

.NET Framework 4.6 and 4.5
Other Versions

Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.

This is incorrect as the event occurs when any item is replaced and has nothing to do with whether or not the entire collection (not list!) is refreshed. It also occurs when the collection is cleared.

The use of the word "changed" is confusing to most users. If changes are made to an existing item, this event will not be fired. This is proper behavior. If you want to know when something on an individual object changes, it is that object's responsibility and not the collection. Depending on the type of objects in the collection, there are notification mechanisms in dependency properties and other methods for this.

When is when? Before or after the collection has been modified? In most cases this detail will not matter, but in some cases it can be important. As we shall see soon, the answer is that it happens after the collection has been modified and not before.

NotifyCollectionChangedAction Enumeration

NotifyCollectionChangedAction Enumeration

Image 2

NotifyCollectionChanged (and thus NotifyCollectionChangedAction as well) are used by more than just ObservableCollection. Other classes in the .NET framework implement it, as do custom user objects.

First it mentions several times "one more more items". For ObservableCollection, this is always false. CollectionChanged never ever references more than a single item of the collection except for the technicality of Replace where it references both the old item and the new item - but in that case to the collection they are actually the same item, just different objects. For ObservableCollection, the NewItems and OldItems arguments never contain more than one item. Other objects that implement INotifyCollectionChanged may pass more than one item.

For the item Reset, the text is also confusing. What is a "dramatic change"? Will the event be called when the ObservableCollection hits 40 and has a mid life crisis?

For ObservableCollection, Reset actually signifies that the collection was cleared of all items by the clear method (and not that items were removed one by one).

For other implementations Reset could signify that the items have been sorted, etc. That is no other action can describe what occured.

Super Express Lane: One Item or Less

While INotifyCollectionChanged and ObservableCollection are usable outside of a WPF UI, they are most commonly used for WPF data binding and were introduced for this task. We have already established that ObservedCollection which is the primary INotifyCollectionChanged implementation never sends more than one item. But, there is another interesting limitation.

The only consumer of INotifyCollectionChanged in .NET currently is WPF data binding. If you attempt to use data binding with an INotifyCollectionChanged object and your implementation passes more than one item, some WPF objects will explicitly throw an exception “Range actions are not supported.”. Thus - even the .NET framework sometimes assumes this interface never passes more than one item. This means that any custom implementation of INotifyCollectionChanged that you intend to use with .NET objects, you too must limit the items to one. (WPF data binding limitation - Credit to William E. Kempf for pointing this additional supporting fact out)

Implementing CollectionChanged

OK so lets get down to the business of subscribing to and implementing a CollectionChanged event.

C#
void Source_CollectionChanged(object aSender, NotifyCollectionChangedEventArgs aArgs) {
}

Now what? Well most people would flesh out a switch block.

C#
void Source_CollectionChanged(object aSender, NotifyCollectionChangedEventArgs aArgs) {
  switch (aArgs.Action) {
    case NotifyCollectionChangedAction.Add:
      break;

    case NotifyCollectionChangedAction.Move:
      break;

    case NotifyCollectionChangedAction.Remove:
      break;

    case NotifyCollectionChangedAction.Replace:
      break;

    case NotifyCollectionChangedAction.Reset:
      break;

    default:
      throw new NotImplementedException();
  }
}

Everything is pretty intuitive and clear up to this point. But when you start to fill in the case blocks you begin to think - What data do I have to account for? So one goes to MSDN, which is of no real help.

To help you out I've written a small demo which demonstrates all the cases and the resulting arguments for CollectionChanged. It is a simple WPF application which creates, populates, and finally uses an ObservableCollection while it logs the arguments and other relevant information. Here is the output from the demo:

mOC[2] = "3";
  Count: 5
  Replace
  System.Collections.ObjectModel.ObservableCollection`1[System.String]
  Count: 5
  Old
    Index: 2
    Three
  New
    Index: 2
    3

mOC.Add("Six");
  Count: 5
  Add
  System.Collections.ObjectModel.ObservableCollection`1[System.String]
  Count: 6
  Old
    Index: -1
    (null)
  New
    Index: 5
    Six

mOC.RemoveAt(2);
  Count: 6
  Remove
  System.Collections.ObjectModel.ObservableCollection`1[System.String]
  Count: 5
  Old
    Index: 2
    3
  New
    Index: -1
    (null)

mOC.Insert(2, "Three");
  Count: 5
  Add
  System.Collections.ObjectModel.ObservableCollection`1[System.String]
  Count: 6
  Old
    Index: -1
    (null)
  New
    Index: 2
    Three

mOC.Move(0, 1);
  Count: 6
  Move
  System.Collections.ObjectModel.ObservableCollection`1[System.String]
  Count: 6
  Old
    Index: 0
    One
  New
    Index: 1
    One

mOC.Clear();
  Count: 6
  Reset
  System.Collections.ObjectModel.ObservableCollection`1[System.String]
  Count: 0
  Old
    Index: -1
    (null)
  New
    Index: -1
    (null)

We can now clearly see that the event is fired after the collection has been modified instead of before. This can be seen by looking at the Count portion of each section above. We can also see that both NewItems and OldItems never contain more than one item, and when there are not items they are null instead of an empty list.

These are all important details which are necessary for implementation of the CollectionChanged event but are not documented and why there are so many questions around what should be a simple implementation.

ObservedCollection

By now I hope I have covered all the gaps in documentation and that anyone can easily implement a CollectionChanged event to monitor changes to an ObservableCollection. But I didn't stop there. I have also created a wrapper class which can be used which disambiguates and provides a cleaner interface to implement ObservableCollection change notifications. I have called it ObservedCollection and use of it is nearly self explanatory and allows much clearer and more stable usage.

Instead of a single event which passes in a circus of arguments which are sometimes used and sometimes not, ObservedCollection demuxes the CollectionChanged event into discrete events that only pass specific relevant arguments eliminating any confusion.

ObservedCollection does not descend from ObservableCollection but instead wraps it. The is because in many cases the ObservableCollection is not created by the developer but in other code. This allows you to use ObservedCollection when you cannot control the creation of it.

ObservedCollection has two constructors. One allows use with an existing ObservableCollection, while the parameterless one will create an internal one implicitly for you. Both versions of the constructor will set the public readonly Source field to the underlying ObservableCollection.

The events that are exposed by ObservedCollection are:

void OnItemAdded(ObservableCollection<T> aSender, int aIndex, T aItem);
void OnItemMoved(ObservableCollection<T> aSender, int aOldIndex, int aNewIndex, T aItem);
void OnItemRemoved(ObservableCollection<T> aSender, int aIndex, T aItem);
void OnItemReplaced(ObservableCollection<T> aSender, int aIndex, T aOldItem, T aNewItem);
void OnCleared(ObservableCollection<T> aSender);

Now instead of a switch and figuring out which arguments are valid, one can simply subscribe to these events.

C#
void xObserved_OnItemReplaced(ObservableCollection<string> aSender, int aIndex, string aOldItem, string aNewItem) {
  mLog.AppendLine("  Replaced @ " + aIndex);
  mLog.AppendLine("  Old: " + aOldItem);
  mLog.AppendLine("  New: " + aNewItem);
  mLog.AppendLine();
}

void xObserved_OnItemRemoved(ObservableCollection<string> aSender, int aIndex, string aItem) {
  mLog.AppendLine("  Removed @ " + aIndex);
  mLog.AppendLine("  " + aItem);
  mLog.AppendLine();
}

void xObserved_OnItemMoved(ObservableCollection<string> aSender, int aOldIndex, int aNewIndex, string aItem) {
  mLog.AppendLine("  Moved from " + aOldIndex + " to " + aNewIndex);
  mLog.AppendLine("  " + aItem);
  mLog.AppendLine();
}

void xObserved_OnItemAdded(ObservableCollection<string> aSender, int aIndex, string aItem) {
  mLog.AppendLine("  Added @ " + aIndex);
  mLog.AppendLine("  " + aItem);
  mLog.AppendLine();
}

void xObserved_OnCleared(ObservableCollection<string> aSender) {
  mLog.AppendLine("  Cleared");
  mLog.AppendLine();
}

Future Expansion

If you wish to use ObservedCollection with another source that sends multiple items, or ObservableCollection is changed in the future, ObservedCollection can be easily modified to loop and fire individual events. It could also be modified to pass multiple events, but still use discrete events to avoid invalid states. BeginUpdate/EndUpdate events could be also be added.

The reset handler should also check to see if Source.Count == 0 in case of other changes in the future.

Demo Source

 

License

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


Written By
Cyprus Cyprus
Chad Z. Hower, a.k.a. Kudzu
"Programming is an art form that fights back"

I am a former Microsoft Regional DPE (MEA) covering 85 countries, former Microsoft Regional Director, and 10 Year Microsoft MVP.

I have lived in Bulgaria, Canada, Cyprus, Switzerland, France, Jordan, Russia, Turkey, The Caribbean, and USA.

Creator of Indy, IntraWeb, COSMOS, X#, CrossTalk, and more.

Comments and Discussions

 
GeneralGood documentation of bad documentation Pin
Sam Hobbs12-Oct-23 8:44
Sam Hobbs12-Oct-23 8:44 
GeneralMy vote of 5 Pin
BillWoodruff21-Sep-22 5:24
professionalBillWoodruff21-Sep-22 5:24 
GeneralMy vote of 5 Pin
Peter Huber SG26-Jul-15 20:48
mvaPeter Huber SG26-Jul-15 20:48 
QuestionVery good article. Pin
Cheung Tat Ming12-Jul-15 21:48
Cheung Tat Ming12-Jul-15 21:48 
GeneralMy vote of 5 Pin
Sumuj John29-Jun-15 13:40
professionalSumuj John29-Jun-15 13:40 
GeneralMy vote of 2 Pin
Paulo Zemek29-Jun-15 10:49
mvaPaulo Zemek29-Jun-15 10:49 
GeneralRe: My vote of 2 Pin
Chad Z. Hower aka Kudzu29-Jun-15 11:12
Chad Z. Hower aka Kudzu29-Jun-15 11:12 
GeneralRe: My vote of 2 Pin
Paulo Zemek29-Jun-15 11:46
mvaPaulo Zemek29-Jun-15 11:46 
GeneralRe: My vote of 2 Pin
Chad Z. Hower aka Kudzu29-Jun-15 12:03
Chad Z. Hower aka Kudzu29-Jun-15 12:03 
GeneralRe: My vote of 2 Pin
Paulo Zemek29-Jun-15 13:48
mvaPaulo Zemek29-Jun-15 13:48 
GeneralRe: My vote of 2 Pin
Peter Huber SG26-Jul-15 20:46
mvaPeter Huber SG26-Jul-15 20:46 
Hi Paulo

It is really sad that you give a 2 for a well written article pointing out correctly the flaw of Microsoft's documentation and design. You qualify that your 2 is only about one particular aspect, but in the end you spoil the rating of the whole article. By this you demotivate people contributing to codeporject. You really shouldn't do that. 2 means it is a bad article. But actually, this is a good article.

Peter
GeneralRe: My vote of 2 Pin
Paulo Zemek26-Jul-15 20:59
mvaPaulo Zemek26-Jul-15 20:59 
GeneralRe: My vote of 2 Pin
Sam Hobbs12-Oct-23 8:31
Sam Hobbs12-Oct-23 8:31 
GeneralMemberName / Description Pin
Paulo Zemek29-Jun-15 7:58
mvaPaulo Zemek29-Jun-15 7:58 
GeneralRe: MemberName / Description Pin
Chad Z. Hower aka Kudzu29-Jun-15 8:05
Chad Z. Hower aka Kudzu29-Jun-15 8:05 
GeneralRe: MemberName / Description Pin
Chad Z. Hower aka Kudzu29-Jun-15 8:07
Chad Z. Hower aka Kudzu29-Jun-15 8:07 
GeneralRe: MemberName / Description Pin
Paulo Zemek29-Jun-15 8:11
mvaPaulo Zemek29-Jun-15 8:11 
GeneralRe: MemberName / Description Pin
William E. Kempf29-Jun-15 8:21
William E. Kempf29-Jun-15 8:21 
GeneralRe: MemberName / Description Pin
Chad Z. Hower aka Kudzu29-Jun-15 8:09
Chad Z. Hower aka Kudzu29-Jun-15 8:09 
GeneralRe: MemberName / Description Pin
Paulo Zemek29-Jun-15 8:16
mvaPaulo Zemek29-Jun-15 8:16 
GeneralRe: MemberName / Description Pin
Chad Z. Hower aka Kudzu29-Jun-15 8:19
Chad Z. Hower aka Kudzu29-Jun-15 8:19 
GeneralRe: MemberName / Description Pin
Paulo Zemek29-Jun-15 8:25
mvaPaulo Zemek29-Jun-15 8:25 
GeneralRe: MemberName / Description Pin
William E. Kempf29-Jun-15 8:33
William E. Kempf29-Jun-15 8:33 
GeneralRe: MemberName / Description Pin
Paulo Zemek29-Jun-15 8:38
mvaPaulo Zemek29-Jun-15 8:38 
GeneralRe: MemberName / Description Pin
Chad Z. Hower aka Kudzu29-Jun-15 12:04
Chad Z. Hower aka Kudzu29-Jun-15 12:04 

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.