ObservableCollection events and WPF Window/UI on Different Threads March 3, 2007
Posted by Karl Hulme in .Net, WPF.trackback
Warning: There’s an improved version of the class outlined in a subsequent blog post.
When binding ObservableCollection’s using WPF you’ll probably encounter the following error, ‘This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.’
This is simply telling you that the CollectionChanged event was not raised on the thread that the Window was created on. It’s an easy problem to fix, we just need to wrap the collection so that any events raised on the source collection will be raised on the same thread as the UI. My complete implementation is below which can be copy/pasted and the namespace renamed.
using System; using System.Collections.Generic; using System.Text; using System.Collections.Specialized; using System.ComponentModel; using System.Windows.Threading; namespace MyNamespace { public class SynchronisedObservableCollection<T>: ICollection<T>, INotifyCollectionChanged, INotifyPropertyChanged { private ICollection<T> collection; private Dispatcher dispatcher; public SynchronisedObservableCollection(ICollection<T> collection) { if (collection == null || collection as INotifyCollectionChanged == null || collection as INotifyPropertyChanged == null) { throw new ArgumentException("Collection must support ICollection, INotifyCollectionChanged " + " and INotifyPropertyChanged interfaces."); } this.collection = collection; this.dispatcher = Dispatcher.CurrentDispatcher; INotifyCollectionChanged collectionChanged = collection as INotifyCollectionChanged; collectionChanged.CollectionChanged += delegate(Object sender, NotifyCollectionChangedEventArgs e) { dispatcher.Invoke(DispatcherPriority.Normal, new RaiseCollectionChangedEventHandler(RaiseCollectionChangedEvent), e); }; INotifyPropertyChanged propertyChanged = collection as INotifyPropertyChanged; propertyChanged.PropertyChanged += delegate(Object sender, PropertyChangedEventArgs e) { dispatcher.Invoke(DispatcherPriority.Normal, new RaisePropertyChangedEventHandler(RaisePropertyChangedEvent), e); }; } #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) { collection.Add(item); } public void Clear() { collection.Clear(); } public bool Contains(T item) { return collection.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { collection.CopyTo(array, arrayIndex); } public int Count { get { return collection.Count; } } public bool IsReadOnly { get { return collection.IsReadOnly; } } public bool Remove(T item) { return collection.Remove(item); } #endregion #region IEnumerable<T> Members public IEnumerator<T> GetEnumerator() { return collection.GetEnumerator(); } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return ((System.Collections.IEnumerable)collection).GetEnumerator(); } #endregion } }
The wrapper above accepts any ICollection<T> that also supports INotifyCollectionChanges and INotifyPropertyChanged. Best candidates for this are the ObservableCollection<T> and ReadOnlyObservableCollection<T> classes. You use the wrapper inbetween your business layer (the original collection) and the UI classes that will consume it. For me, this means it sits in class that provides central access to all Model data (thinking MVC/MVP)
public static class AppModel { public static SynchronisedObservableCollection<String> GetSyncCollection() { ObservableCollection<String> innerCollection = new ObservableCollection<String>(); return new SynchronisedObservableCollection<String>(innerCollection); } }
You can now bind to the collection using an ObjectDataProvider that calls the GetSyncCollection method. Note that my implementation doesn’t require you to pass in the Dispatcher of the UI thread. That’s because I assume that the UI thread will be the one creating the wrapper. Since this class forms part of the UI layer and not the business layer, then it’s a fair assumption, but if it doesn’t work for you then you can always pass a Dispatcher into the constructor.
<Window x:Class="MyApplication.Host" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:model="clr-namespace:MyNamespace.Model;assembly=MyAssembly.Model" Width="800" Height="600" > <Window.Resources> <ObjectDataProvider x:Key="myList" ObjectType="model:AppModel" MethodName="GetSyncCollection"/> </Window.Resources> <ListBox ItemsSource="{Binding Source={StaticResource myList}}"> </Window>
Incidently, I’m not particularly happy with SynchronisedObservableCollection as the class name. It implies that the collection is thread-safe, and although in one way it is, it many others it isn’t!
Comments»
No comments yet — be the first.