jump to navigation

SynchronizedObservableCollection and BindableCollection March 4, 2007

Posted by Karl Hulme in .Net, WPF.
trackback

This blog is no longer actively maintained. The content is well over 5 years old – which is like 50 coder years. Use at your own risk!

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! 

About these ads

Comments»

1. ObservableCollection events and WPF Window/UI on Different Threads « Atomic Blography - March 4, 2007

[…] 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. […]

2. PsiSpace by Simon Middlemiss WPF Collections & Threads « - March 6, 2007

[…] 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. […]

3. Chip - April 7, 2007

Did you measure the performance impact of using this class versus ObservableCollection?

4. John Schroedl - April 11, 2007

Sweet! This is exactly what I was looking for. Thanks for sharing!

John

5. Karl Hulme - April 16, 2007

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.

6. pat - May 23, 2007

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.

7. Karl Hulme - June 1, 2007

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!!

8. Patrick Waters - June 25, 2007

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); //

M B - January 16, 2012

I’m getting this as well.

9. Patrick Waters - June 25, 2007

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?

10. Miral - October 16, 2007

I’ve hit the same problem as Patrick. The problem is that the Add operation of a SynchronizedObservableCollection acquires the lock, then does the insertion, which raises CollectionChanged. The BindableCollection then picks that up, and Invokes the collection changed handler, which tries to acquire the lock too (and deadlocks, because the original thread is still holding it and is blocked waiting for the UI thread invoke to complete).

This doesn’t appear solvable. You can’t release the lock before invoking the CollectionChanged, because that will cause race conditions and mismatches in the notification data vs. the collection data. You can’t change the Invoke to a BeginInvoke (which would avoid the deadlock) for the same reason. You can’t use a non-synchronised collection because then it’s no longer threadsafe at all.

It really looks like the only thing that can be done is to only update data-bound collections in the UI thread.

11. Thoughts from Mirality » Multithreaded Collections and WPF - October 29, 2007

[…] your own synchronisation. If you’re not really sure where to start with that, then using this SynchronizedObservableCollection is probably the best bet. (Note that the BindableCollection shown on the same page has the serious […]

12. Andy - February 5, 2008

Hi Karl, nice classes.

Any chance you can put up an example utilizing these in a Model View Provider pattern simple solution?

Thanks!

13. Draven - September 24, 2008

Hi,

I have a problem with accesing ObservableCollection from different Threads, one from WPF and one from normal code, inside OnCollectionChanged event a dispatcher object raises the handler it refresh UI interface but if the collection changed from another thread (normal code by example) the OnCollectionChanged event is not raised, any suggestions?. Thx.

14. Viewmodelling lists « Notify Changed - January 30, 2009

[…] ways to get it better. Note: the ideas for the mirrorcollection class are taken form here and here, althoug the problen that they focus is quite different: in short it is that WPF does not react to […]

15. Using MVVM to provide undo/redo. Part 2: Viewmodelling lists « Notify Changed - January 30, 2009

[…] ways to get it better. Note: the ideas for the mirrorcollection class are taken form here and here, althoug the problen that they focus is quite different: in short it is that WPF does not react to […]

16. TWiStErRob - February 7, 2009

Re 8-9: I changed Invoke to BeginInvoke on event handler dispatching and the deadlock gone away.

17. Claudio Alvarez - May 31, 2009

Nice work Karl! However, I lose the line breaks when copying your code… Is it too much of a hassle to supply a zip file containing the classes? Why doesn’t any blog do that anyway?

18. Claudio Alvarez - May 31, 2009

Hmmmm.. IE8 1, Ffox3 0. FFox can’t copy properly, what a shame.

19. Mertsch - June 12, 2009

Hello, this article helped me a lot BUT
I really had the feeling that you were reinventing the wheel and unwilling to implement such a huge class for such a simple purpose. I went through a lot of trial and error and came up with a solution, which IMHO is much clearer/cleaner then yours.

First we make our observable collection synchronized …

public class SynchronizedObservableCollection : ObservableCollection
{
private readonly Object snycRoot = new object();

protected Object SyncRoot
{
get { return this.snycRoot; }
}

protected override void ClearItems()
{
lock (this.snycRoot)
{
base.ClearItems();
}
}

protected override void InsertItem(int index, T item)
{
lock (this.snycRoot)
{
base.InsertItem(index, item);
}
}

protected override void MoveItem(int oldIndex, int newIndex)
{
lock (this.snycRoot)
{
base.MoveItem(oldIndex, newIndex);
}
}

protected override void RemoveItem(int index)
{
lock (this.snycRoot)
{
base.RemoveItem(index);
}
}

protected override void SetItem(int index, T item)
{
lock (this.snycRoot)
{
base.SetItem(index, item);
}
}
}

then we make it bindable

public class BindableObservableCollection : SynchronizedObservableCollection
{
private readonly Dispatcher dispatcher = Dispatcher.CurrentDispatcher;

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
this.dispatcher.Invoke(new Action(() => base.OnCollectionChanged(e)), null);
}

protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
this.dispatcher.Invoke(new Action(() => base.OnPropertyChanged(e)), null);
}
}

Yes, that is all there is to it and it works (at least for me)!

greets

Aaron Walker - February 16, 2012

The issue that I see with this smaller implementation is you have not synchronized all of the properties and methods from the inherited Collection base class. For example, calls to Add or Remove are completely unsynchronized and are not thread safe.

I could be completely wrong here, but the complete implementation is necessary as I see it.

20. NotifyChanged » Blog Archive » Using MVVM to provide undo/redo. Part 2: Viewmodelling lists - May 25, 2010

[…] ways to get it better. Note: the ideas for the mirrorcollection class are taken form here and here, althoug the problen that they focus is quite different: in short it is that WPF does not react to […]

21. Jay - March 7, 2013

@Mertsch,

Thanks for a great simplication.

I would also note that the ObservableCollection must not be *Created or Altered* by any thread other than the Main UI Thread, else, it can throw the Exception on the Main Thread, or cause Thread-Lock.

I have added the following methods to your BindableObservableCollection class:

///
///
/// Provides Property update on the main Dispatcher thread.
/// Uses .
///
protected override void InsertItem(int index, T item)
{
this.dispatcher.InvokeIfRequired(() => base.InsertItem(index, item));
}

///
protected override void MoveItem(int oldIndex, int newIndex)
{
this.dispatcher.InvokeIfRequired(() => base.MoveItem(oldIndex, newIndex));
}

///
protected override void RemoveItem(int index)
{
this.dispatcher.InvokeIfRequired(() => base.RemoveItem(index));
}

///
protected override void SetItem(int index, T item)
{
this.dispatcher.InvokeIfRequired(() => base.SetItem(index, item));
}

And created a helper class:

///
/// Dispatcher Extensions
///
public static class DispatcherExtensions
{
///
/// Invokes if required.
///
/// The dispatcher.
/// The action.
public static void InvokeIfRequired(this Dispatcher dispatcher, Action action)
{
if (dispatcher.CheckAccess())
{
action();
}
else
{
dispatcher.Invoke(action, null);
}
}

///
/// Invokes if required.
///
/// The type of the return value.
/// The dispatcher.
/// The func.
///
public static TReturn InvokeIfRequired(this Dispatcher dispatcher, Func func)
{

if (dispatcher.CheckAccess())
{
return (TReturn)func();
}
else
{
return (TReturn)dispatcher.Invoke(func);
}
}
}

You also have to ensure that any changes to the “View” take care to use the correct dispatcher.
This is an update of the SetActiveWorkspace() method found in many example WorkspaceViewModels:
(eg. WPF Model-View-ViewModel Toolkit http://wpf.codeplex.com/releases/view/14962)

///
/// The dispatcher
///
protected readonly Dispatcher dispatcher = Dispatcher.CurrentDispatcher;

///
/// Sets the active workspace (View).
///
/// The workspace.
///
/// ObservableCollections bound to WPF Controls should only be created /
/// altered by the Main (UI) Thread:
/// – All access to View Workspaces Collection must be on the main UI Thread.
/// – All Views must be created on the main UI Thread, or Dispatcher object set to
/// – NotSupportedException Exceptions or Thread-Lock will result if
/// a Bound Collection is Modified / Created on a different Thread.
/// correct instance on creation.
///
protected void SetActiveWorkspace(WorkspaceViewModel workspace)
{
Debug.Assert(this.Workspaces.Contains(workspace));

ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.Workspaces);
if (collectionView != null)
{
this.dispatcher.InvokeIfRequired(() => collectionView.MoveCurrentTo(workspace));
}
}

22. visit site - May 8, 2013

Rattling nice pattern and wonderful content material , very little else we require : D.

23. Larue - September 23, 2013

I am genuinely delighted to read tuis weblog posts
which carries tons of useful data, thanks for providing these kinds of information.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: