Friday, 16 May 2014

This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread

This System.NotSupportedException exception is raised when we attempt to modify (e.g. add to) a collection (e.g. an ObservableCollection) on a thread which isn't the same thread that created the collection (e.g. the UI thread).

In short: An ObservableCollection, for example, can only be modified in the thread that created it.

In WPF this thread affinity is handled by System.Windows.Threading.Dispatcher -- hence it's reference in the error message.

Typically a WPF project has a single Dispatcher instance so to modify the collection we'd have to invoke the modification while we're running in the UI (Dispatcher) thread.

We could use Dispatcher.CurrentDispatcher.Invoke (or BeginInvoke) to do the addition, however, with .Net 4.5 we now have System.Windows.Data.BindingOperations.EnableCollectionSynchronization.

EnableCollectionSynchronization 'Enables a collection to be accessed across multiple threads and specifies the lock object that should be used to synchronize access to the collection.'.

For example, I had a TraceListener which I wanted to be invoked from any number of threads.

The basis of it now looks like this:



namespace Example
{
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Windows.Data;
 
  /// <summary>
  /// A simple trace listener which tracks the written events in a list.
  /// </summary>
  /// <typeparam name="T">This is the type of the list that will be created and added to for each <see cref="WriteLine"/>.</typeparam>
  internal class StringListListener<T> : TraceListener
    where T : IList<string>, new()
  {
    /// <summary>
    /// <see cref="BindingOperations.EnableCollectionSynchronization(System.Collections.IEnumerable,object)"/> uses this to synchronise access to the list
    /// </summary>
    private readonly object _syncLock = new object();
 
    /// <summary>List of current events</summary>
    private readonly T _events;
 
    /// <summary>Cache the current line if <see cref="Write"/> is called.</summary>
    private string _currentLine;
 
    /// <summary>Initializes a new instance of the <see cref="StringListListener{T}"/> class.</summary>
    public StringListListener()
    {
      this._events = new T();
      BindingOperations.EnableCollectionSynchronization(this._events, this._syncLock);
    }
 
    /// <summary>Gets a reference to the underlying list of current events.</summary>
    public T Events
    {
      get
      {
        return this._events;
      }
    }
 
    /// <summary>Override the <see cref="TraceListener.Write(string)"/> method to capture the text being written.</summary>
    /// <param name="message">The message being traced.</param>
    public override void Write(string message)
    {
      this._currentLine += message;
    }
 
    /// <summary>Override the <see cref="TraceListener.WriteLine(string)"/> method to capture the line being written.</summary>
    /// <param name="message">The message being traced.</param>
    public override void WriteLine(string message)
    {
      foreach (var line in (this._currentLine + message).Split(new[] { Environment.NewLine }, StringSplitOptions.None))
      {
        this._events.Add(line);
      }

      this._currentLine = string.Empty;
    }
  }
}

I can now use it like this:

var listener = new StringListListener<ObservableCollection<string>>();
 
// Keep track of the each line using an observable collection (for example)
this.Events = listener.Events;
 
Trace.Listeners.Add(listener);

No comments: