Question

When updating a collection of business objects on a background thread I get this error message:

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

Ok, that makes sense. But it also begs the question, what version of CollectionView does support multiple threads and how do I make my objects use it?

Was it helpful?

Solution

The following is an improvement on the implementation found by Jonathan. Firstly it runs each event handler on the dispatcher associated with it rather than assuming that they are all on the same (UI) dispatcher. Secondly it uses BeginInvoke to allow processing to continue while we wait for the dispatcher to become available. This makes the solution much faster in situations where the background thread is doing lots of updates with processing between each one. Perhaps more importantly it overcomes problems caused by blocking while waiting for the Invoke (deadlocks can occur for example when using WCF with ConcurrencyMode.Single).

public class MTObservableCollection<T> : ObservableCollection<T>
{
    public override event NotifyCollectionChangedEventHandler CollectionChanged;
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler CollectionChanged = this.CollectionChanged;
        if (CollectionChanged != null)
            foreach (NotifyCollectionChangedEventHandler nh in CollectionChanged.GetInvocationList())
            {
                DispatcherObject dispObj = nh.Target as DispatcherObject;
                if (dispObj != null)
                {
                    Dispatcher dispatcher = dispObj.Dispatcher;
                    if (dispatcher != null && !dispatcher.CheckAccess())
                    {
                        dispatcher.BeginInvoke(
                            (Action)(() => nh.Invoke(this,
                                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
                            DispatcherPriority.DataBind);
                        continue;
                    }
                }
                nh.Invoke(this, e);
            }
    }
}

Because we are using BeginInvoke, it is possible that the change being notified is undone before the handler is called. This would typically result in an "Index was out of range." exception being thrown when the event arguments are checked against the new (altered) state of the list. In order to avoid this, all delayed events are replaced with Reset events. This could cause excessive redrawing in some cases.

OTHER TIPS

Use:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    {
         // Your Action Code
    });

This post by Bea Stollnitz explains that error message and why it's worded the way it is.

EDIT: From Bea's blog

Unfortunately, this code results in an exception: “NotSupportedException – This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.” I understand this error message leads people to think that, if the CollectionView they’re using doesn’t support cross-thread changes, then they have to find the one that does. Well, this error message is a little misleading: none of the CollectionViews we provide out of the box supports cross-thread collection changes. And no, unfortunately we can not fix the error message at this point, we are very much locked down.

Found one.

public class MTObservableCollection<T> : ObservableCollection<T>
{
   public override event NotifyCollectionChangedEventHandler CollectionChanged;
   protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
   {
      var eh = CollectionChanged;
      if (eh != null)
      {
         Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                 let dpo = nh.Target as DispatcherObject
                 where dpo != null
                 select dpo.Dispatcher).FirstOrDefault();

        if (dispatcher != null && dispatcher.CheckAccess() == false)
        {
           dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
        }
        else
        {
           foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
              nh.Invoke(this, e);
        }
     }
  }
}

http://www.julmar.com/blog/mark/2009/04/01/AddingToAnObservableCollectionFromABackgroundThread.aspx

You can also look at: BindingOperations.EnableCollectionSynchronization.

See Upgrading to .NET 4.5: An ItemsControl is inconsistent with its items source

Sorry, can't add a comment but all this is wrong.

ObservableCollection is not thread safe. Not only because of this dispatcher issues, but it's not thread safe at all (from msdn):

Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

Look here http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx

There's also a problem when calling BeginInvoke with a "Reset" action. "Reset" is the only action where handler should look at the collection itself. If you BeginInvoke a "Reset" and then immediately BeginInvoke a couple of "Add" actions than handler will accept a "Reset" with already updated collection and next "Add"'s will create a mess.

Here's my implementation which works. Actually I'm thinking of removing BeginInvoke at all:

Fast performing and thread safe observable collection

You can get wpf to manage cross thread changes to a collection by enabling collection synchronization like so:

BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;

This tells WPF that the collection may be modified off the UI thread so it knows it has to marshal any UI changes back to the appropriate thread.

There is also an overload to provide a synchronization callback if you don't have a lock object.

If you want to update WPF UI Control periodically and at the same time use UI you can use DispatcherTimer.

XAML

<Grid>
        <DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Name="dgDownloads" VerticalAlignment="Top" Width="548" />
        <Label Content="" Height="28" HorizontalAlignment="Left" Margin="0,221,0,0" Name="lblFileCouner" VerticalAlignment="Top" Width="173" />
</Grid>

C#

 public partial class DownloadStats : Window
    {
        private MainWindow _parent;

        DispatcherTimer timer = new DispatcherTimer();

        ObservableCollection<FileView> fileViewList = new ObservableCollection<FileView>();

        public DownloadStats(MainWindow parent)
        {
            InitializeComponent();

            _parent = parent;
            Owner = parent;

            timer.Interval = new TimeSpan(0, 0, 1);
            timer.Tick += new EventHandler(timer_Tick);
            timer.Start();
        }

        void timer_Tick(object sender, EventArgs e)
        {
            dgDownloads.ItemsSource = null;
            fileViewList.Clear();

            if (_parent.contentManagerWorkArea.Count > 0)
            {
                foreach (var item in _parent.contentManagerWorkArea)
                {
                    FileView nf = item.Value.FileView;

                    fileViewList.Add(nf);
                }
            }

            if (fileViewList.Count > 0)
            {
                lblFileCouner.Content = fileViewList.Count;
                dgDownloads.ItemsSource = fileViewList;
            }
        }   

    }

Try This:

this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{

 //Code

}));

None of them, just use Dispatcher.BeginInvoke

Here's a VB version I made after some googling and slight mods. Works for me.

  Imports System.Collections.ObjectModel
  Imports System.Collections.Specialized
  Imports System.ComponentModel
  Imports System.Reflection
  Imports System.Windows.Threading

  'from: http://stackoverflow.com/questions/2137769/where-do-i-get-a-thread-safe-collectionview
  Public Class ThreadSafeObservableCollection(Of T)
    Inherits ObservableCollection(Of T)

    'from: http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/listcollectionviewcollectionview-doesnt-support-notifycollectionchanged-with-multiple-items.aspx
    Protected Overrides Sub OnCollectionChanged(ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs)
      Dim doit As Boolean = False

      doit = (e.NewItems IsNot Nothing) AndAlso (e.NewItems.Count > 0)
      doit = doit OrElse ((e.OldItems IsNot Nothing) AndAlso (e.OldItems.Count > 0))

      If (doit) Then
        Dim handler As NotifyCollectionChangedEventHandler = GetType(ObservableCollection(Of T)).GetField("CollectionChanged", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(Me)
        If (handler Is Nothing) Then
          Return
        End If

        For Each invocation As NotifyCollectionChangedEventHandler In handler.GetInvocationList
          Dim obj As DispatcherObject = invocation.Target

          If (obj IsNot Nothing) Then
            Dim disp As Dispatcher = obj.Dispatcher
            If (disp IsNot Nothing AndAlso Not (disp.CheckAccess())) Then
              disp.BeginInvoke(
                Sub()
                  invocation.Invoke(Me, New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
                End Sub, DispatcherPriority.DataBind)
              Continue For
            End If
          End If

          invocation.Invoke(Me, e)
        Next
      End If
    End Sub
  End Class

Small mistake in the VB version. Just replace :

Dim obj As DispatcherObject = invocation.Target

By

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top