Creating a Custom Observable Collection in WPF
One of the biggest advantages WPF has is its Data Binding features. Data Binding can be done in several ways, but the most common is using the INotifyPropertyChanged and INotifyCollectionChanged interfaces. For simple objects, all you need to do is implement the INotifyPropertyChanged, and raise the PropertyChanged event when your properties change. Maybe something like this:
public class MySampleClass : INotifyPropertyChanged
{
public int MyProperty
{
get
{
return _myProperty;
}
set
{
if (_myProperty == value)
return;
_myProperty = value;
OnPropertyChanged("MyProperty");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Instead of having each class implement the interface, you can create a base class that already does that, and inherit from this new one. You can see an example on this post. However, remember that you can only inherit from one class, so make sure you structure you class hierarchy properly. My advice is to use this only in presentation objects (or view models, if you’re using MVVM)
When you want to bind to a collection, you will want to use ObservableCollection<T>. This collection implements both interfaces, therefore notifying you when the items in the collection change and when the items’ properties change. This works great if you’re using standard collections. The problem arises when you want to use a custom collection. When you create an ObservableCollection<T> on v3.5, you have 3 constructors that rely on Collection<T> to create a new list. If you pass a List<T> or IEnumerable<T>, all that it does is copy the items, between both collections. This will of course make you custom collection useless. For this to work, you will have to create a new “Observable” wrapper around your custom collection. Lets start with the interface. All we need is a collection that implements the two interfaces we discussed earlier.
public interface ICustomObservableCollection<T> : ICollection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
}
Before we move on the the implementation, first we need to better understand what is it that we’re trying to do and how to achieve it. We have a custom collection that we need to add additional features, so we need to “decorate” our collection with new behavior. For this we can follow the Decorator Pattern.
public class CustomObservableCollection<T> : ICustomObservableCollection<T>
{
#region Fields
...
#endregion
#region Properties
protected ICollection<T> InnerCollection { get; private set; }
#endregion
#region Constructors
public CustomObservableCollection(ICollection<T> innerCollection)
{
if (innerCollection == null)
throw new ArgumentNullException("innerCollection");
InnerCollection = innerCollection;
}
#endregion
#region Implementation
#region Implementation of INotifyCollectionChanged
...
#endregion
#region Implementation of INotifyPropertyChanged
...
#endregion
#region Implementation of IEnumerable
...
#endregion
#region Implementation of ICollection<T>
...
#endregion
#endregion
}
Now that we have both collections wrapped on inside the other, all we need to do is provide the implementation of the interfaces. Starting with INotifyPropertyChanged:
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
private void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
#endregion
Then, we can implement INotifyCollectionChanged. For this implementation to work correctly, we need to add a monitor, so that we detect collection reentrancy. We can do this with this SimpleMonitor class, that implements IDisposable, and just keeps a counter of entries. We also need to provide methods tor activating the monitor.
#region SimpleMonitor
protected IDisposable BlockReentrancy()
{
this._monitor.Enter();
return this._monitor;
}
protected void CheckReentrancy()
{
if ((this._monitor.Busy && (CollectionChanged != null)) && (CollectionChanged.GetInvocationList().Length > 1))
{
throw new InvalidOperationException("Collection Reentrancy Not Allowed");
}
}
[Serializable]
private class SimpleMonitor : IDisposable
{
private int _busyCount;
public bool Busy
{
get { return this._busyCount > 0; }
}
public void Enter()
{
this._busyCount++;
}
#region Implementation of IDisposable
public void Dispose()
{
this._busyCount--;
}
#endregion
}
#endregion
This SimpleMonitor is created in the CustomObservableCollection constructor, so we need to account for that too.
#region Fields
private readonly SimpleMonitor _monitor;
#endregion
#region Constructors
public CustomObservableCollection(ICollection innerCollection)
{
this._monitor = new SimpleMonitor();
if (innerCollection == null)
{
throw new ArgumentNullException("innerCollection");
}
InnerCollection = innerCollection;
}
#endregion
Now, we’re ready to provide an implementation for INotifyCollectionChanged.
#region Implementation of INotifyCollectionChanged
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
{
using (BlockReentrancy())
{
CollectionChanged(this, e);
}
}
}
private void OnCollectionChanged(NotifyCollectionChangedAction action, object item)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item));
}
private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
}
private void OnCollectionReset()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion
The next one is IEnumerable. For this one, we will delegate to the InnerCollection.
#region Implementation of IEnumerable
public IEnumerator GetEnumerator()
{
return InnerCollection.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
Finally, provide the implementation for ICollection<T>. Again, this will delegate to the InnerCollection raising events when needed. Notice the use of the CountString and IndexerName fields.
#region Fields private const string CountString = "Count"; private const string IndexerName = "Item[]"; private readonly SimpleMonitor _monitor; #endregion
#region Implementation of ICollection
public void Add(T item)
{
CheckReentrancy();
InnerCollection.Add(item);
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnCollectionChanged(NotifyCollectionChangedAction.Add, item, InnerCollection.Count);
}
public void Clear()
{
CheckReentrancy();
InnerCollection.Clear();
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnCollectionReset();
}
public bool Contains(T item)
{
return InnerCollection.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
InnerCollection.CopyTo(array, arrayIndex);
}
public bool Remove(T item)
{
CheckReentrancy();
bool result = InnerCollection.Remove(item);
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnCollectionChanged(NotifyCollectionChangedAction.Remove, item);
return result;
}
public int Count
{
get { return InnerCollection.Count; }
}
public bool IsReadOnly
{
get { return InnerCollection.IsReadOnly; }
}
#endregion
You can download the source code here.
Update: Added the source code as a compressed file. You can get it here.
