2013-08-09

Silverlight: ObservableCollection.CollectionChanged Event

The ObservableCollection<T>.CollectionChanged event will only fire if the collection itself changes (items are added, removed or rearranged/moved.), NOT if an item in the collection changes (meaning one of the item's in the collection has one or more properties modified.)

It's now around 2:30am, and I've been going the rounds for a couple of hours with a Silverlight project at work; fighting a seemingly simple change that ended up not being so simple because of a major misunderstanding of the CollectionChanged event on the ObservableCollection<T>.  My ObservableCollection<T>.CollectionChanged event was not firing like I thought it should.

I imagine this will be a common mistake for others, and I'm sure I myself am doomed to repeat it again in the future; so I'm documenting it for a quick reminder later.

My thought process was this... "I need to watch my ObservableCollection for one of its items to be changed.  Once the item is changed, I need to flip a boolean flag which happens to be bound to a command which will disable a button after the change has been made."

Simple, right?
I should just be able to handle the ObservableCollection<T>.CollectionChanged event, because if one of those items in the collection gets changed, that counts... 

Or not.

See, the misconception is, the collection itself hasn't changed.  It has the exact same items it had before, it's just that the item within the collection changed.  So there are two things that can happen to your collection:

  • The collection itself is modified (Items are added, removed or moved.)
  • An item in the collection is modified (its individual properties are changed.)

Let me illustrate:
For this example we will have a class called VideoGame to keep track of titles and costs.

public class VideoGame : INotifyPropertyChanged
{
 private string _title;
 public string Title 
 { 
  get { return _title; } 
  set
  {
   _title = value;
   RaisePropertyChanged("Title");
  }
 }
 
 private decimal _cost;
 public decimal Cost 
 { 
  get { return _cost; } 
  set
  {
   _cost = value;
   RaisePropertyChanged("Cost");
  }
 }
 
 public event PropertyChangedEventHandler PropertyChanged;
 public void RaisePropertyChanged(string propertyName)
 {
  if(PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }
}

And now some code to use it...

ObservableCollection<VideoGame> videoGames = new ObservableCollection<VideoGame>
{
 new VideoGame{ Title = "TrackMania 2: Stadium", Cost = 9.99M },
 new VideoGame{ Title = "TrackMania 2: Canyon", Cost = 19.99M },
 new VideoGame{ Title = "TrackMania 2: Valley", Cost = 19.99M },
};

videoGames.CollectionChanged += (sender, eventArgs) => {
 
 Debug.WriteLine("--- The collection changed");
};

Debug.WriteLine("Changing the cost of one of the video games in the collection...");
// The CollectionChanged event will not fire because it was just one of the collection's items that changed...
// not the collection itself.
videoGames[0].Cost = 7.99M; // the game went on sale.

Debug.WriteLine("Adding a video game to the collection...");
// The CollectionChanged event WILL fire because the collection of items changed.
videoGames.Add(new VideoGame{ Title ="ShootMania: Storm", Cost = 19.99M });

And the output...
Changing the cost of one of the video games in the collection...
Adding a video game to the collection...
--- The collection changed
When it dawned on me, at first I was frustrated; but then clarity set in.  It makes sense to me that we only want the CollectionChanged event to fire when the collection itself is modified, not its individual parts.  With it, the NotifyCollectionChangedEventArgs that are passed with the event give us a lot of good information about what happened with the change.

  • Action - Tells which action effected the change in the collection (Add, Remove, Replace, Reset)
  • NewItems - The items affected by the action
  • OldItems - The items removed or replaced in the collection
  • NewStartingIndex - Tells the index where the change occurred.
  • OldStartingIndex - Tells where a Replace or Remove action took place.
While all this is cool, what do I need to have happen so I can track when an item in the collection is changed?

I need to make sure I handle PropertyChanged on the individual items of the collection.  Make note, this can be tricky because I still need to watch for the Collection to change so I can subscribe to any new items' PropertyChanged events as they are added to the collection, and clean up after myself when the items leave the collection.

I'll let you decide how you're going to approach that; it's a discussion for another day.  But I want to drive home the concept that the CollectionChanged event is just that... when the collection itself changes, not its individual items.  Know that, and you'll save yourself a lot of time.

No comments: