Advanced Basics

The ObservableCollection Class

Ken Getz

Code download available from the Download Center

Contents

Introducing the ObservableCollection Class
Examining the Sample
Examining the Code

Imagine that you're creating a Windows Forms application, and you have bound a DataGridView control to a standard List(Of Customer) data structure. You'd like to be able to keep the items in the grid synchronized with the values in the underlying data source. That is, if some other code or some other form changes a customer's data in the List, you'd like the grid to update and display the modified data.

Using Windows Forms, this feat is possible, in a very general sense. You can make the update work, but it will have severe limitations. For example, you can, under the right circumstances, see the updates immediately in the grid, but there's no easy way to add a new row to the grid when someone adds a new row to the data source. Windows Presentation Foundation (WPF) adds functionality to the Microsoft .NET Framework so that you actually can reliably keep bound controls synchronized with their data sources. Here I'll demonstrate how to use the ObservableCollection class provided by WPF.

Although the ObservableCollection class makes it possible for WPF applications to keep bound controls in sync with underlying data sources, it provides even more useful information—specifically, the ObservableCollection class also raises the CollectionChanged event when you add, remove, move, refresh, or replace an item in the collection. This feature makes it possible to react when code outside your window modifies the underlying data. You'll see how you might use this information in this month's sample application, which I'll describe next.

Introducing the ObservableCollection Class

The System.Collections.ObjectModel.ObservableCollection(Of T) class inherits from Collection(Of T), which is the base class for generic collections, and implements both the INotifyCollectionChanged and INotifyPropertyChanged interfaces. It's the INotifyCollectionChanged interface that makes the collection interesting—this is the interface that allows bound objects (and your code) to determine if the collection has changed.

It's important to note that although the ObservableCollection class broadcasts information about changes to its elements, it doesn't know or care about changes to the properties of its elements. In other words, it doesn't watch for property change notification on the items within its collection.

If you need to know if someone has changed a property of one of the items within the collection, you'll need to ensure that the items in the collection implement the INotifyPropertyChanged interface, and you'll need to manually attach property changed event handlers for those objects. No matter how you change properties of objects within the collection, the collection's PropertyChanged event will not fire. As a matter of fact, the ObservableCollection's PropertyChanged event handler is protected—you can't even react to it unless you inherit from the class and expose it yourself. You could, of course, handle the PropertyChanged event for each item within the collection from your inherited collection, although in the sample application I take a simpler approach, allowing the client application to handle individual items' change events.

If you disregard inherited and protected members (assuming that you are already familiar with the base Collection class, from which all of the members of the ObservableCollection class derive), the only interesting members left are the Move method (which allows you to move a member to a new location within the collection) and the CollectionChanged event (which broadcasts information about changes to the contents of the collection). Before reading further, you might want to download and install the sample WPF application that demonstrates these features.

Examining the Sample

The sample solution, ObservableCollectionTest, contains a class named CustomerList, which inherits from ObservableCollection (see Figure 1). As you might imagine, the CustomerList class exposes an ObservableCollection instance containing Customer objects. If you examine the code, however, you'll see that the class exposes only one list, so that multiple consumers of the class each retrieve a reference to the same collection. (That's the point of this particular demonstration, and isn't necessary for other applications.) The class provides a private constructor, so the only way to retrieve an instance of the class is to call the shared GetList method, which hands out the existing collection instance:

Private Shared list As New CustomerList Public Shared Function GetList() As CustomerList Return list End Function

Figure 1 CustomerList

System.Collections.ObjectModel Imports System.ComponentModel Public Class CustomerList Inherits ObservableCollection(Of Customer) Private Shared list As New CustomerList Public Shared Function GetList() As CustomerList Return list End Function Private Sub New() ' Make the constructor private, enforcing the "factory" concept ' the only way to create an instance of this class is by calling ' the GetList method. AddItems() End Sub Public Shared Sub Reset() list.ClearItems() list.AddItems() End Sub Private Sub AddItems() Add(New Customer("Maria Anders")) Add(New Customer("Ana Trujillo")) Add(New Customer("Antonio Moreno")) End Sub End Class

The private constructor calls the AddItems method; the public shared Reset method clears the list and then calls the AddItems method. Either way, the result is three customers in the collection:

Private Sub AddItems() Add(New Customer("Maria Anders")) Add(New Customer("Ana Trujillo")) Add(New Customer("Antonio Moreno")) End Sub

The Customer class, for this example, is extremely simple (that is, just complex enough to demonstrate the necessary features). The class, shown in Figure 2, contains only a Name property and wouldn't even be worth discussing if it weren't for the fact that the class implements the INotifyPropertyChanged interface, which enables consumers of instances of the class (including data-bound controls) to be notified when property values change.

Figure 2 The Customer Class with the PropertyChanged Event

Imports System.ComponentModel Public Class Customer Implements INotifyPropertyChanged Public Event PropertyChanged( _ ByVal sender As Object, _ ByVal e As PropertyChangedEventArgs) _ Implements INotifyPropertyChanged.PropertyChanged Protected Overridable Sub OnPropertyChanged( _ ByVal PropertyName As String) ' Raise the event, and make this procedure ' overridable, should someone want to inherit from ' this class and override this behavior: RaiseEvent PropertyChanged( _ Me, New PropertyChangedEventArgs(PropertyName)) End Sub Public Sub New(ByVal Name As String) ' Set the backing field so that you don't raise the ' PropertyChanged event when you first create the Customer. _name = Name End Sub Private _name As String Public Property Name() As String Get Return _name End Get Set(ByVal value As String) If _name <> value Then _name = value OnPropertyChanged("Name") End If End Set End Property End Class

When a class implements the INotifyPropertyChanged interface, it must supply the PropertyChanged event:

Public Event PropertyChanged( _ ByVal sender As Object, _ ByVal e As PropertyChangedEventArgs) _ Implements INotifyPropertyChanged.PropertyChanged

In order to raise the event using the standard .NET design pattern, the Customer class includes a protected, overridable OnPropertyChanged procedure, which raises the event:

Protected Overridable Sub OnPropertyChanged( _ ByVal PropertyName As String) ' Raise the event, and make this procedure ' overridable, should someone want to inherit from ' this class and override this behavior: RaiseEvent PropertyChanged( _ Me, New PropertyChangedEventArgs(PropertyName)) End Sub

Then, within the definition for the Name property, the property setter calls the OnPropertyChanged method if the new value is different from the current value of the property:

Private _name As String Public Property Name() As String Get Return _name End Get Set(ByVal value As String) If _name <> value Then _name = value OnPropertyChanged("Name") End If End Set End Property

Given the PropertyChanged event that the class raises, it's possible for code that uses the class, or a collection of instances of the class, to react to the PropertyChanged event and take action based on the fact that the property changed. (Please note that the PropertyChangedEventArgs class adds only a PropertyName property to the standard event arguments and doesn't provide any information on either the old or the new value of the property. The sample application works around this limitation, as you'll see, to at least determine the new value of the changed property.)

The sample also contains a single WPF window named MainWindow, as shown in Figure 3. The only important detail in the markup for the window lies in the definition for the ListBox control, which includes declarative data binding for the ItemsSource property of the control. The binding indicates that the control should get its data from a property of the MainWindow class named Data and that it should display the Name property of each item within the Data property:

<ListBox DisplayMemberPath="Name" ItemsSource= "{Binding ElementName=MainWindow, Path=Data}" Grid.Column="3" Grid.RowSpan="3" Name="ItemListBox" Margin="5" />

fig03.gif

Figure 3 The Sample WPF Window(Click the image for a larger view)

The codebehind class for MainWindow includes the following declaration:

Public WithEvents Data As CustomerList = CustomerList.GetList()

This code exposes the contents of a CustomerList instance to the window, as you see in Figure 3.

Before examining the remainder of the code within the code­behind class, you should stop at this point and experiment with the application. Because the ListBox on the window has been bound to a class that inherits from ObservableCollection, you would expect that the listbox should always stay current with the contents of the collection, and the demonstration window proves this to be true.

In addition, the sample displays two separate instances of the main window, and because the ListBox controls on both windows are bound to the same ObservableCollection instance, changes you make in one window appear in both windows. To open two instances of the window, the Application.xaml file contains the following markup, indicating that the application should start by running the code in the Application_Startup procedure:

<Application x:Class="Application" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Startup="Application_Startup"> <Application.Resources> </Application.Resources> </Application>

The Application.xaml.vb codebehind file contains the following startup code, which creates two instances of MainWindow.xaml, each with its own title:

Private Sub Application_Startup( _ ByVal sender As System.Object, _ ByVal e As System.Windows.StartupEventArgs) Dim window As New MainWindow window.Title = "Observable Collection 1" window.Show() window = New MainWindow window.Title = "Observable Collection 2" window.Show() End Sub

Follow these steps to give the sample application a workout.

  1. Within Visual Studio 2008, load and run the sample application. You'll see two instances of the same window.
  2. Click to open the combobox on the left side of the window. Note that the control contains the numbers 0 through 2, corresponding to the three current customers. Choose 1, which selects Ana Trujillo and copies her name into the textbox.
  3. In one window, click Delete. Ana Trujillo disappears from both windows, because the two ListBox controls are bound to the same ObservableCollection instance, and the binding causes updates to appear immediately. Open the combobox again, and note that it now indicates that there are only two customers. Try it in both windows to verify that both instances are up-to-date.
  4. Click Delete two more times, removing all the customers. Click Reset Data to refill the lists in both windows.
  5. In the textbox next to the Add New Item button, enter your own name and click Add New Item. The new name appears in both ListBox controls. Click to open the combobox and verify that it now contains numbers from 0 to 3 (one for each of the four customers). Verify that the combobox has changed on both windows—obviously, both windows' classes received the event that indicated that the collection had changed.
  6. In the ListBox in one window, select a name. In the lower textbox on the left, modify the name and click Change. First, you'll see an alert indicating that you have changed the property, and once you dismiss it, you'll see that the name has changed in both windows (see Figure 4).

fig04.gif

Figure 4 Capturing Change Events for Data in the Collection(Click the image for a larger view)

Except for the alert that appeared when you changed the customer name and the ComboBox control whose items change to match the number of customers in the collection, all the code in the sample window deals with the window's user interface. That is, all the work done to keep the ListBox control in sync with the ObservableCollection instance happens for "free" and is managed by WPF.

When you add a new item, the ListBox automatically displays the full list. When you change an item, the ListBox automatically displays the modified list. When you delete items, the ListBox maintains full fidelity with the underlying collection. In other words, in terms of binding the ObservableCollection class to controls in WPF, you can easily say "It just works."

Actually, there is some "magic" going on under the covers. When you bind the ListBox to the ObservableCollection, WPF actually creates a CollectionView instance, which presents a view of the data that handles grouping, sorting, filtering, and so on. You're seeing that default view of the collection within the ListBox controls. You could create multiple CollectionView instances based on the same collection and display the data in different ways (sorted, for example) within one of the ListBox controls. Although it's outside the scope of this discussion, it's worth investigating the CollectionView class if you need to display multiple views of the same collection. For more information, please look at the CollectionView Classinformation on MSDN.

Examining the Code

The MainWindow class defines the CustomerList instance using the WithEvents keyword so that the code can handle events of the ObservableCollection list without requiring the code to add handlers manually:

Public WithEvents Data As CustomerList = CustomerList.GetList()

In the Change Event Handlers region of the code, you'll find the CollectionChanged event handler, which verifies that you have either added or removed an item from the collection. If so, the code sets the combobox's data source and enables buttons on the window accordingly, as you see in Figure 5.

Figure 5 Check for Collection Changed

Private Sub Data_CollectionChanged( _ ByVal sender As Object, _ ByVal e As NotifyCollectionChangedEventArgs) _ Handles Data.CollectionChanged ' Because the collection raises this event, you can modify your user ' interface on any window that displays controls bound to the data. On ' both windows, if you add or remove an item, all the controls update ' to indicate the new collection! ' Did you add or remove an item in the collection? If e.Action = NotifyCollectionChangedAction.Add Or _ e.Action = NotifyCollectionChangedAction.Remove Then ' Set the list of integers in the combo box: SetComboDataSource() ' Enable buttons as necessary: EnableButtons() End If End Sub

The important part of this simple code lies in the NotifyCollectionChangedEventArgs parameter. This object provides information about what changed within the collection and also provides five interesting properties, which are listed in Figure 6.

Figure 6 NotifyCollectionChangedEventArgs Parameters

Parameter Description
Action Retrieves information about the action that caused the event. This property contains a NotifyCollectionChangedAction value, which can be either Add, Remove, Replace, Move, or Reset.
NewItems Retrieves a list of the new items that were involved in the change to the collection.
NewStartingIndex Retrieves the index of the collection at which the change occurred.
OldItems Retrieves a list of the old items that were affected by the Replace, Remove, or Move action.
OldStartingIndex Retrieves the index of the collection at which the Replace, Remove, or Move action occurred.

Given all this information, your event handler can determine exactly what occurred within the collection that triggered the event. The sample code merely uses the Action property, updating the list of integers in the ComboBox control if the collection changed size:

If e.Action = NotifyCollectionChangedAction.Add Or _ e.Action = NotifyCollectionChangedAction.Remove Then

Although it's not pertinent to the discussion of the ObservableCollection class, it is interesting to see how the code fills in the list of indexes in the ComboBox control:

Private Sub SetComboDataSource() ' Set the list of integers shown in the ' combo box: ItemComboBox.ItemsSource = _ Enumerable.Range(0, Data.Count) End Sub

Rather than performing some loop to generate a list containing integers between 0 and the highest-numbered index in the collection, this code simply calls the Enumerable.Range method to retrieve a collection of integers starting at 0 and containing Data.Count values. The code sets the ItemsSource property of the ComboBox control to the returned collection—and that's all there is to it! (If you are looking for more information on the Enumerable class, you should read my previous two Advanced Basics columns: The LINQ Enumerable Class, Part 1and The LINQ Enumerable Class, Part 2.)

In order for the sample application to be notified if you change properties of an item within the collection, you have to write a little more code. Each of the properties within the class raises the PropertyChanged event each time some code changes the value of the property. (Of course, it's up to the author of the specific class to make sure that property changes raise the PropertyChanged event, because it doesn't happen automatically. The Customer class's Name property, as you've seen, does this.)

Within the MainWindow class, you'll find the HookupChangeEventHandler procedure, which hooks up the PropertyChanged event for an individual Customer object:

Private Sub HookupChangeEventHandler(ByVal cust As Customer) ' Add a PropertyChanged event handler for ' the specified Customer instance: AddHandler cust.PropertyChanged, _ AddressOf HandlePropertyChanged End Sub

The HookupChangeEventHandlers procedure hooks up event handlers for each Customer within the ObservableCollection of customers, like so:

Private Sub HookupChangeEventHandlers() For Each cust As Customer In Data HookupChangeEventHandler(cust) Next End Sub

When the window loads or you click the Reset button, the code calls the HookupChangeEventHandlers procedure. When you click Delete, the window both removes the item from the collection and removes the event handler:

' From DeleteItemButton_Click Dim index As Integer = ItemComboBox.SelectedIndex If index >= 0 Then RemoveHandler Data.Item(index).PropertyChanged, _ AddressOf HandlePropertyChanged Data.RemoveAt(index)

When you click Add New Item, the code creates the new customer, and it also hooks up its PropertyChanged event:

' From NewItemButton_Click cust = New Customer(NewItemTextBox.Text) HookupChangeEventHandler(cust) Data.Add(cust)

Of course, because the window binds the ListBox control to the ObservableCollection instance, all these changes appear automatically, without any code support, in both instances of the window. In fact, you only need the code support to programmatically add and remove customers from the list, and to hook up and react to changes within an individual customer.

When you do change the value of a property within the Customer class, the client application receives the notification via the PropertyChanged event handler. Note that the HandlePropertyChanged procedure (shown in Figure 7) contains the application's most complex code. Because the change notification supplies only the name of the changed property, you must remember that it's up to your code to retrieve the current value of the property, should you have some need for it.

Figure 7 HandlePropertyChanged Using Reflection

Private Sub HandlePropertyChanged( _ ByVal sender As Object, _ ByVal e As PropertyChangedEventArgs) ' In this particular application, you only want to bother with this ' code for the first window, although both will run the code. In this ' case, if the event was raised by the window whose title is ' "Observable Collection 1" then process the event: If Me.Title.EndsWith("1") Then Dim propName As String = e.PropertyName Dim myCustomer As Customer = CType(sender, Customer) ' Unfortunately, no one hands you the old property value, or the new ' property value. You can use Reflection to retrieve the new property ' value, given the object that raised the event and the name of the ' property: Dim propInfo As System.Reflection.PropertyInfo = _ GetType(Customer).GetProperty(propName) Dim value As Object = _ propInfo.GetValue(myCustomer, Nothing) MessageBox.Show(String.Format( _ "You changed the property '{0}' to '{1}'", _ propName, value)) End If End Sub

The procedure starts by ensuring that the code only runs once—because you have two instances of the window open, the code would otherwise run for each instance, and there's no point in seeing the alert twice. The code simply checks the final character of the title (assuming that you haven't changed the window's Title property) and restricts the behavior to a single window:

If Me.Title.EndsWith("1") Then 'Code removed here… End If

The code retrieves and stores the name of the property that was changed and a reference to the object that raised the event—that is, the current customer:

Dim propName As String = e.PropertyName Dim myCustomer As Customer = CType(sender, Customer)

The code then uses Reflection to retrieve a System.Reflection.PropertyInfo instance, given the name of the property and the type:

Dim propInfo As System.Reflection.PropertyInfo = _ GetType(Customer).GetProperty(propName)

Given the PropertyInfo object and a particular Customer instance, the code can then retrieve the current value of the property:

Dim value As Object = _ propInfo.GetValue(myCustomer, Nothing)

The remainder of the code in the application maintains the user interface, including enabling/disabling buttons, keeping the combobox and listbox in sync, and so on.

Although this application made use of the binding support provided by the ObservableCollection class and also reacted to its CollectionChanged event in order to update the user interface, you needn't use the class this way. Because it notifies listeners that its contents have changed, you can replace any List or Collection instance that you use with an ObservableCollection instance (even if you're not creating a WPF application) and then hook up event handlers to notify clients that the collection's contents have changed.

Just as the sample window updated the list of integers corresponding to collection indexes when the collection changed size, you could react in any necessary way to changes in the collection from a client class. Remember, however, that the collection won't tell you, on its own, whether the properties of its child elements have changed. You'll need to hook up event handlers within the client, so the client gets notified when the properties of a child element within the collection change.

Also remember that the rich data binding support that you saw in the sample application only works within WPF applications—if you're creating a Windows Forms application, you'll still need to manually refresh the binding for any control that's bound to an ObservableCollection instance when the collection changes. On the other hand, because the collection notifies you when it changes, at least it's now possible to make this happen.

Send your questions and comments for Ken to basics@microsoft.com.

Ken Getzis a Senior Consultant with MCW Technologies and a courseware author for AppDev ( appdev.com). He is coauthor of ASP .NET Developers Jumpstart, Access Developer's Handbook, and VBA Developer's Handbook, 2nd Edition. Reach him at keng@mcwtech.com.