SynchronizedObservableCollection and BindableCollection March 4, 2007
Posted by Karl Hulme in .Net, WPF.trackback
Yesterday I posted some code that marshals the events raised by an ObservableCollection onto the same thread as is running the UI layer. I called it a SynchronizedObservableCollection but said at the end of the post that I was unhappy with the name. Well, while continuing to work on my project I’ve found need for a genuinely synchronized version of the ObservableCollection class. I also need a ReadOnly version – predominately as a base class so that I can add the methods I want for altering the collection, rather than the standard Add/Remove. On top of these two, I still need to be able to bind them both to controls in WPF and not worry about which thread is causing the collections to change.
Therefore, the classes I need are:
- SynchronizedObservableCollection
- ReadOnlySynchronizedObservableCollection
- BindableCollection
The SynchronizedObservableCollection is a collection that will accept updates and changes from lot’s of different threads without complaining. It’s implemented the same way as the SynchronizedCollection class provided in the framework, e.g. you can provide your own syncRoot or one will be created. You can also provide an existing list if you wish, but this is not a wrapper class, so if you do pass a list to the constructor it will be copied and any changes made to one list will not be reflected in the other.
[ComVisible(false)] public class SynchronizedObservableCollection<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable, INotifyPropertyChanged, INotifyCollectionChanged { private ObservableCollection<T> items; private Object sync; #region Constructors public SynchronizedObservableCollection(Object syncRoot, IEnumerable<T> list) { this.sync = (syncRoot == null) ? new Object() : syncRoot; this.items = (list == null) ? new ObservableCollection<T>() : new ObservableCollection<T>(new List<T>(list)); items.CollectionChanged += delegate(Object sender, NotifyCollectionChangedEventArgs e) { OnCollectionChanged(e); }; INotifyPropertyChanged propertyChangedInterface = items as INotifyPropertyChanged; propertyChangedInterface.PropertyChanged += delegate(Object sender, PropertyChangedEventArgs e) { OnPropertyChanged(e); }; } public SynchronizedObservableCollection(object syncRoot) : this(syncRoot, null) { } public SynchronizedObservableCollection(): this(null, null) { } #endregion #region Methods public void Add(T item) { lock (this.sync) { int index = this.items.Count; this.InsertItem(index, item); } } public void Clear() { lock (this.sync) { this.ClearItems(); } } protected virtual void ClearItems() { this.items.Clear(); } public bool Contains(T item) { lock (this.sync) { return this.items.Contains(item); } } public void CopyTo(T[] array, int index) { lock (this.sync) { this.items.CopyTo(array, index); } } public IEnumerator<T> GetEnumerator() { lock (this.sync) { return this.items.GetEnumerator(); } } public int IndexOf(T item) { lock (this.sync) { return this.InternalIndexOf(item); } } public void Insert(int index, T item) { lock (this.sync) { if ((index < 0) || (index > this.items.Count)) { throw new ArgumentOutOfRangeException("index", index, "Value must be in range."); } this.InsertItem(index, item); } } protected virtual void InsertItem(int index, T item) { this.items.Insert(index, item); } private int InternalIndexOf(T item) { int count = this.items.Count; for (int i = 0; i < count; i++) { if (object.Equals(this.items[i], item)) { return i; } } return -1; } public bool Remove(T item) { lock (this.sync) { int index = this.InternalIndexOf(item); if (index < 0) { return false; } this.RemoveItem(index); return true; } } public void RemoveAt(int index) { lock (this.sync) { if ((index < 0) || (index >= this.items.Count)) { throw new ArgumentOutOfRangeException("index", index, "Value must be in range."); } this.RemoveItem(index); } } protected virtual void RemoveItem(int index) { this.items.RemoveAt(index); } protected virtual void SetItem(int index, T item) { this.items[index] = item; } void ICollection.CopyTo(Array array, int index) { lock (this.sync) { for (int i = 0; i < items.Count; i++) { array.SetValue(items[i], index + i); } } } IEnumerator IEnumerable.GetEnumerator() { return this.items.GetEnumerator(); } int IList.Add(object value) { VerifyValueType(value); lock (this.sync) { this.Add((T)value); return (this.Count - 1); } } bool IList.Contains(object value) { VerifyValueType(value); return this.Contains((T)value); } int IList.IndexOf(object value) { VerifyValueType(value); return this.IndexOf((T)value); } void IList.Insert(int index, object value) { VerifyValueType(value); this.Insert(index, (T)value); } void IList.Remove(object value) { VerifyValueType(value); this.Remove((T)value); } private static void VerifyValueType(object value) { if (value == null) { if (typeof(T).IsValueType) { throw new ArgumentException("Synchronized collection wrong type null."); } } else if (!(value is T)) { throw new ArgumentException("Synchronized collection wrong type."); } } #endregion #region Properties public int Count { get { lock (this.sync) { return this.items.Count; } } } public T this[int index] { get { lock (this.sync) { return this.items[index]; } } set { lock (this.sync) { if ((index < 0) || (index >= this.items.Count)) { throw new ArgumentOutOfRangeException("index", index, "Value must be in range."); } this.SetItem(index, value); } } } protected ObservableCollection<T> Items { get { return this.items; } } public object SyncRoot { get { return this.sync; } } bool ICollection<T>.IsReadOnly { get { return false; } } bool ICollection.IsSynchronized { get { return true; } } object ICollection.SyncRoot { get { return this.sync; } } bool IList.IsFixedSize { get { return false; } } bool IList.IsReadOnly { get { return false; } } object IList.this[int index] { get { return this[index]; } set { VerifyValueType(value); this[index] = (T)value; } } #endregion #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { if (PropertyChanged != null) PropertyChanged(this, e); } #endregion #region INotifyCollectionChanged Members public event NotifyCollectionChangedEventHandler CollectionChanged; protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (CollectionChanged != null) CollectionChanged(this, e); } #endregion }
The ReadOnlySynchronizedObservableCollection is a wrapper class that expects a SynchronizedObservableCollection as the only constructor parameter. Because it’s a wrapper class, you could maintain a SynchronizedObservableCollection in one layer of your app, and just expose a ReadOnlySynchronizedObservableCollection to the other layers. Similarly, as a read only class you wouldn’t expect change events, hence the INotifyPropertyChanged and INotifyCollectionChanged interfaces are implemented explicitly (again following the pattern of the ReadOnlyObservableCollection). Don’t forget you can always consume the events by casting the object to the interfaces of course, and for this reason, it doesn’t affect WPF binding.
[ComVisible(false)] public class ReadOnlySynchronizedObservableCollection<T> : ReadOnlyCollection<T>, INotifyPropertyChanged, INotifyCollectionChanged { #region Constructor public ReadOnlySynchronizedObservableCollection(SynchronizedObservableCollection<T> list): base(list) { list.PropertyChanged += delegate(Object sender, PropertyChangedEventArgs e) { OnPropertyChanged(e); }; list.CollectionChanged += delegate(Object sender, NotifyCollectionChangedEventArgs e) { OnCollectionChanged(e); }; } #endregion #region Event Handling private NotifyCollectionChangedEventHandler collectionChanged; private PropertyChangedEventHandler propertyChanged; protected event NotifyCollectionChangedEventHandler CollectionChanged { add { collectionChanged += value; } remove { collectionChanged -= value; } } protected event PropertyChangedEventHandler PropertyChanged { add { propertyChanged += value; } remove { propertyChanged -= value; } } event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged { add { collectionChanged += value; } remove { collectionChanged -= value; } } event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged { add { propertyChanged += value; } remove { propertyChanged -= value; } } protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { if (propertyChanged != null) { propertyChanged(this, e); } } protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (collectionChanged != null) { collectionChanged(this, e); } } #endregion }
Finally, the BindableCollection is the class I started building yesterday. This class can be bound to a WPF ItemsControl (or similar) and then be updated on any thread. Among some minor coding changes, I’ve added support for the non-generic interfaces (e.g. IList as well as IList<T>) and I’ve allowed the Dispatcher to be passed in. This is necessary if the BindableCollection is not created on the UI thread. I’ve also dropped the word ‘Observable’ from the class name on the grounds that the class must be Observable if it is to be Bindable, making that bit superflous. The BindableCollection is a wrapper class and will accept anything that implements IList<T>, INotifyCollectionChanged and INotifyPropertyChanged. However, most commonly it will be fed an ObservableCollection, ReadOnlyObservableCollection, SynchronizedObservableCollection, ReadOnlySynchronizedObservableCollection or a class derived from one of these.
public class BindableCollection<T>: ICollection<T>, IList<T>, IEnumerable<T>, ICollection, IList, IEnumerable, INotifyCollectionChanged, INotifyPropertyChanged { private IList<T> list; private Dispatcher dispatcher; #region Constructor public BindableCollection(IList<T> list) : this(list, null) { } public BindableCollection(IList<T> list, Dispatcher dispatcher) { if (list == null || list as INotifyCollectionChanged == null || list as INotifyPropertyChanged == null) { throw new ArgumentNullException("The list must support IList, INotifyCollectionChanged " + "and INotifyPropertyChanged."); } this.list = list; this.dispatcher = (dispatcher == null) ? Dispatcher.CurrentDispatcher : dispatcher; INotifyCollectionChanged collectionChanged = list as INotifyCollectionChanged; collectionChanged.CollectionChanged += delegate(Object sender, NotifyCollectionChangedEventArgs e) { this.dispatcher.Invoke(DispatcherPriority.Normal, new RaiseCollectionChangedEventHandler(RaiseCollectionChangedEvent), e); }; INotifyPropertyChanged propertyChanged = list as INotifyPropertyChanged; propertyChanged.PropertyChanged += delegate(Object sender, PropertyChangedEventArgs e) { this.dispatcher.Invoke(DispatcherPriority.Normal, new RaisePropertyChangedEventHandler(RaisePropertyChangedEvent), e); }; } #endregion #region INotifyCollectionChanged Members public event NotifyCollectionChangedEventHandler CollectionChanged; private void RaiseCollectionChangedEvent(NotifyCollectionChangedEventArgs e) { if (CollectionChanged != null) { CollectionChanged(this, e); } } private delegate void RaiseCollectionChangedEventHandler(NotifyCollectionChangedEventArgs e); #endregion #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; private void RaisePropertyChangedEvent(PropertyChangedEventArgs e) { if (PropertyChanged != null) { PropertyChanged(this, e); } } private delegate void RaisePropertyChangedEventHandler(PropertyChangedEventArgs e); #endregion #region ICollection<T> Members public void Add(T item) { list.Add(item); } public void Clear() { list.Clear(); } public bool Contains(T item) { return list.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { list.CopyTo(array, arrayIndex); } public int Count { get { return list.Count; } } public bool IsReadOnly { get { return list.IsReadOnly; } } public bool Remove(T item) { return list.Remove(item); } #endregion #region IList<T> Members public int IndexOf(T item) { return list.IndexOf(item); } public void Insert(int index, T item) { list.Insert(index, item); } public void RemoveAt(int index) { list.RemoveAt(index); } public T this[int index] { get { return list[index]; } set { list[index] = value; } } #endregion #region IEnumerable<T> Members public IEnumerator<T> GetEnumerator() { return list.GetEnumerator(); } #endregion #region ICollection Members void ICollection.CopyTo(Array array, int index) { ((ICollection)list).CopyTo(array, index); } int ICollection.Count { get { return ((ICollection)list).Count; } } bool ICollection.IsSynchronized { get { return ((ICollection)list).IsSynchronized; } } object ICollection.SyncRoot { get { return ((ICollection)list).SyncRoot; } } #endregion #region IList Members int IList.Add(object value) { return ((IList)list).Add(value); } void IList.Clear() { ((IList)list).Clear(); } bool IList.Contains(object value) { return ((IList)list).Contains(value); } int IList.IndexOf(object value) { return ((IList)list).IndexOf(value); } void IList.Insert(int index, object value) { ((IList)list).Insert(index, value); } bool IList.IsFixedSize { get { return ((IList)list).IsFixedSize; } } bool IList.IsReadOnly { get { return ((IList)list).IsReadOnly; } } void IList.Remove(object value) { ((IList)list).Remove(value); } void IList.RemoveAt(int index) { ((IList)list).RemoveAt(index); } object IList.this[int index] { get { return ((IList)list)[index]; } set { ((IList)list)[index] = value; } } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return ((System.Collections.IEnumerable)list).GetEnumerator(); } #endregion }
If anyone can make use of the above classes then you’re very welcome to use them, although you do so at your own risk!
[...] ObservableCollection events and WPF Window/UI on Different Threads March 3, 2007 Posted by Karl Hulme in WPF, .Net. trackback Warning: There’s an improved version of the class outlined in this post here. [...]
[...] March 6th, 2007 — Simon Middlemiss Karl seems to have a couple of good articles (here & here) on dealing with wpf collections from threads other than the UI. Check them out. [...]
Did you measure the performance impact of using this class versus ObservableCollection?
Sweet! This is exactly what I was looking for. Thanks for sharing!
John
Hi Chip,
No I haven’t done any profiling with any of the classes above. (oh the shame!) Which class were you referring to, and what’s the problem you’re having at the moment?
ps Sorry for the delay in responding – have been on holiday.
hi,
I have a problem that I was hoping to solve with your class but I am still having the same issue.
Basically, I am writing my own observable list that represent a remote collection (located on some server on the net). All I have at time of creation is a list of ID and no data.
I am keeping a list of integer (my ids) and the count method return the number of ids. I am using the items collection for the downloaded object.
When the listbox access an item via the operator [] I insert return a bogus object in the collection and I return it, then I spawn a thread and go download the data from the server. When the data is retrieved, I replace the bogus data in the items collection with the downloaded data. This triggers a replace event.
The listbox, when use with a VirtualizeStackPanel or any other virtualize panel (I wrote some), will replace the data fine until it removes data that is no longer visible. After that, I don’t always the update to work! This is really frustrating!
Hope you have any idea.
Hi Pat,
So you’re saying that with a virtualizing panel, once a replaced item has scrolled out of view, it then doesn’t display properly when scrolled back into view? Is that right?
How/where are the templates defined? – presumably you have one for the bogus object and one for the real one.
Also, I’m not clear on what the end result should be. You can set an ObjectDataProvider to load a collection aynchronously, but I gather you want to load specific items on demand, but with only an ID for each item how can the user determine which one they want loaded – and if they’re all in a StackPanel how does the user signal which one to load. I’m sorry but I’m a bit confused!!
I tried to use these classes in my application where there are two threads accessing the collections. One from a network updating object state and the other from the UI thread, manipulating the same collections.
I keep finding my app getting stuck, (not responding to input) and if i break in visual studio, it’s here:
private void RaiseCollectionChangedEvent(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
{
CollectionChanged(this, e); //
oops. I typed a fake left arrow, <–, to mark the line on the top of the stack and it seemed to snip off the rest of my comment.
I was hoping you’d have some insight as to how i am misusing your classes to cause them to deadlock like this?