(Print this page)

.Net Observable Collections
Published date: Sunday, July 26, 2015
On: Moer and Éric Moreau's web site

Recently, somebody produce an article on the topic of Observable collections and I remembered I had issue with them in a previous project. So I went through his article to find out if/how he would work around the issue I had.

The MSDN definition of this class reads like this: “Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed”.

The issue I had was that when the content of an item of the collection was modified, no events were raised. The encapsulation that the guy made wasn’t solving that issue neither. So I decided to complete the encapsulation and to share it with you.

The downloadable demo code

This month code was created with Visual Studio 2015 (RC) but it is working with Visual Studio 2013 as well. The code is available in both VB and C#.

Figure 1: The demo application in action

Details of my issue

So in one of my project I had this observable collection of class instance. Like the definition states above, I was able to detect when instances were added and removed from the collection. But when a single property of an instance already in the collection was modified, I wasn’t warned by any events and my process was just not able to launch another related process. That kind of sucks!

Some will argue that it is not the job of collection to detect if something in one of the instance was changed. That might be true for many circumstances but not every time.

One good thing about the other article

If you look at the documentation of the ObservableCollection object in MSDN under the Events part, you will find that only one event can be raised by that object: CollectionChanged.

One thing I liked about the article of that other guy is that his abstraction is handling that event and raises more granular events (OnItemAdded, OnItemRemoved, …) with related arguments (index position, new value, previous value, …). I really liked that. I kept that part.

A new ObservedCollection class

To achieve what I need, we will start by first wrapping the ObservableCollection in a new ObservedCollection much like this:

Imports System.Collections.ObjectModel
Imports System.Collections.Specialized
Imports System.ComponentModel

Public Class ObservedCollection(Of T)

    ReadOnly _source As ObservableCollection(Of T)

    Public Sub New(ByVal aSource As ObservableCollection(Of T))
        _source = aSource
        AddHandler _source.CollectionChanged, AddressOf Source_CollectionChanged
    End Sub

    Public Event OnItemAdded(aSender As ObservableCollection(Of T), aIndex As Integer, aItem As T)
    Public Event OnItemMoved(aSender As ObservableCollection(Of T), aOldIndex As Integer, aNewIndex As Integer, aItem As T)
    Public Event OnItemRemoved(aSender As ObservableCollection(Of T), aIndex As Integer, aItem As T)
    Public Event OnItemReplaced(aSender As ObservableCollection(Of T), aIndex As Integer, aOldItem As T, aNewItem As T)
    Public Event OnCleared(aSender As ObservableCollection(Of T))

    Private Sub Source_CollectionChanged(sender As Object, aArgs As NotifyCollectionChangedEventArgs)
        Select Case aArgs.Action
            Case NotifyCollectionChangedAction.Add
                RaiseEvent OnItemAdded(_source, aArgs.NewStartingIndex, DirectCast(aArgs.NewItems(0), T))

            Case NotifyCollectionChangedAction.Move
                RaiseEvent OnItemMoved(_source, aArgs.OldStartingIndex, aArgs.NewStartingIndex, DirectCast(aArgs.NewItems(0), T))

            Case NotifyCollectionChangedAction.Remove
                RaiseEvent OnItemRemoved(_source, aArgs.OldStartingIndex, DirectCast(aArgs.OldItems(0), T))

            Case NotifyCollectionChangedAction.Replace
                RaiseEvent OnItemReplaced(_source, aArgs.OldStartingIndex, DirectCast(aArgs.OldItems(0), T), DirectCast(aArgs.NewItems(0), T))

            Case NotifyCollectionChangedAction.Reset
                RaiseEvent OnCleared(_source)

            Case Else
                Throw New NotImplementedException
        End Select
    End Sub

End Class

This implementation is enough to start demonstrating the issue.

At this point, it is just taking the CollectionChanged event and spits more specific events.

Testing with a collection of strings

Probably the easiest way of testing the observable collections it to create a collection of strings to play with it.

Figure 2: Testing with strings

So I created a WPF window like the one shown in figure 2. To that window, I have added this code to the constructor to create an instance of the observable collection wrapper and to subscribe to events:

Private ReadOnly _observedCollection As ObservableCollection(Of String)

Sub New()
    ' This call is required by the designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    DataContext = Me

    _observedCollection = New ObservableCollection(Of String)
    Dim specializedCollection = New ObservedCollection(Of String)(_observedCollection)
    AddHandler specializedCollection.OnCleared, AddressOf SpecializedCollection_OnCleared
    AddHandler specializedCollection.OnItemAdded, AddressOf SpecializedCollection_OnItemAdded
    AddHandler specializedCollection.OnItemMoved, AddressOf SpecializedCollection_OnItemMoved
    AddHandler specializedCollection.OnItemRemoved, AddressOf SpecializedCollection_OnItemRemoved
    AddHandler specializedCollection.OnItemReplaced, AddressOf SpecializedCollection_OnItemReplaced
End Sub

In each event handler, I have added code to report the action to the listbox control on the left of the screen. The datagrid on the right is bound to a property (full code is downloadable from the link at the top of this article).

When you click on the buttons at the top, you see that the related event is described on the left of the screen and the grid on the right reflects the changes.

Even when you click the Replace button, you find the listbox and the datagrid to be refreshed. So at this point, you wander what I was complaining about! Read on. The interesting part is coming!

Testing with a collection of class instances

You might want to put strings in an observable collection but more realistically, you will store class instances.

So I created a very simple TypeX class like this:

Option Strict On

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Public Class TypeX
    Implements INotifyPropertyChanged

    Private _id As Integer
    Private _name As String

    Public Property Id() As Integer
        Get
            Return _id
        End Get
        Set
            _id = Value
            OnPropertyChanged()
        End Set
    End Property

    Public Property Name() As String
        Get
            Return _name
        End Get
        Set
            _name = Value
            OnPropertyChanged()
        End Set
    End Property


    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

End Class

You will notice that this class even if it is simple, it at least implements the INotifyPropertyChanged interface because after all, we want to be notified when a property of an instance is modified.

I then created a new window to test with the wrapper pointed to an observable collection of my new TypeX. The constructor now reads like this:

Private ReadOnly _observedCollection As ObservableCollection(Of TypeX)

Sub New()

    ' This call is required by the designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    DataContext = Me

    _observedCollection = New ObservableCollection(Of TypeX)
    Dim specializedCollection = New ObservedCollection(Of TypeX)(_observedCollection)
    AddHandler specializedCollection.OnCleared, AddressOf SpecializedCollection_OnCleared
    AddHandler specializedCollection.OnItemAdded, AddressOf SpecializedCollection_OnItemAdded
    AddHandler specializedCollection.OnItemMoved, AddressOf SpecializedCollection_OnItemMoved
    AddHandler specializedCollection.OnItemRemoved, AddressOf SpecializedCollection_OnItemRemoved
    AddHandler specializedCollection.OnItemReplaced, AddressOf SpecializedCollection_OnItemReplaced
End Sub

Figure 3: Testing with TypeX

Now if you run the demo application and click the first 5 buttons (Clear to Replace), as expected the listbox and the datagrid will be updated accordingly.

But when you look at the details of what the Replace button is doing, you will find that it is replacing the full instance of the object stored in the collection:

Private Sub buttonReplace_Click(sender As Object, e As RoutedEventArgs)
    If _observedCollection.Any() Then
        _lastId += 1
        _observedCollection(0) = New TypeX() With {.Id = _lastId, .Name = DateTime.Now.ToLongTimeString()}
    Else
        MessageBox.Show("At least 1 item is required before replacing!")
    End If
End Sub

This is not what happens in real applications when you only want to change a property of an instance. You will most surely have code like this:

Private Sub buttonReplaceContent_Click(sender As Object, e As RoutedEventArgs)
    If _observedCollection.Any() Then
        _observedCollection(0).Name = DateTime.Now.ToLongTimeString()
    Else
        MessageBox.Show("At least 1 item is required before replacing!")
    End If
End Sub

If you only had this code, you would find that the datagrid gets updated with the new value but the listbox on the left remains unchanged. That was exactly my issue. The instance correctly gets updated but no notification that something was changed from the collection perspective.

Fixing the issue

So I returned to my wrapper to find a way to raise an event when a property was modified.

First thing to do is to declare an event like this:

Public Event OnContentChanged(aSender As ObservableCollection(Of T), aItem As T)

Then we need to find out how to trigger that event. Remember that our TypeX is implementing the INotifyPropertyChanged interface. That means that at some point, I would like to write this code:

Private Sub ObjectInstance_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
    RaiseEvent OnContentChanged(_source, DirectCast(sender, T))
End Sub

It turns out that Source_CollectionChanged event knows when instances are added and/or deleted. We just need to hook up the property changed event of each instances. Instead of repeating the code each time this can happen (when adding and when replacing), I created this small method to do the job.

Private Sub SubscribePropertyChanged(aList As IList)
    Dim objectInstance = TryCast(DirectCast(aList(0), T), INotifyPropertyChanged)
    If objectInstance IsNot Nothing Then
        AddHandler objectInstance.PropertyChanged, AddressOf ObjectInstance_PropertyChanged
    End If
End Sub

Notice here that if your class does not implement the INotifyPropertyChanged interface, it just won’t work. When an instance is added or replaced, you can then call this method like this:

SubscribePropertyChanged(aArgs.NewItems)

The same applies to unsubscribing from these events when instances are removed from the collection. If you want to be a good citizen, you should unsubscribe the events. The downloadable application does that.

Now, the test window can handle the new event and the listbox can report the action.

Conclusion

The base classes are not always providing everything you need and the missing parts are not always very complicated to implement.

.Net Generics also allows you to create reusable classes fitting much more than just a single type.


(Print this page)