68

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?

abatishchev
  • 92,232
  • 78
  • 284
  • 421
Jonathan Allen
  • 63,625
  • 65
  • 234
  • 426
  • 1
    Try the following link which provides a thread-safe solution that works from any thread and can be bound to via multiple UI threads : http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection – Anthony Apr 15 '14 at 19:24

12 Answers12

88

Use:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    {
         // Your Action Code
    });
abatishchev
  • 92,232
  • 78
  • 284
  • 421
luke
  • 881
  • 1
  • 6
  • 2
64

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.

Nathan Phillips
  • 10,671
  • 1
  • 26
  • 20
  • 1
    Bit late and an old topic but this bit of code has saved me alot of headaches, thanks! :) – KingTravisG Jan 11 '13 at 01:07
  • Caliburn also has a really nice implementation in their BindableCollection. Take a look here: http://caliburn.codeplex.com/SourceControl/changeset/view/e80dd6b444f4#src/Caliburn.PresentationFramework/BindableCollection.cs – Stephanvs Apr 25 '13 at 09:51
  • I am getting an exception using this version, but not when using the version provided by Jonathan. Does anyone have ideas why this is happening? Here is my InnerException: This exception was thrown because the generator for control 'System.Windows.Controls.DataGrid Items.Count:3' with name 'OrdersGrid' has received sequence of CollectionChanged events that do not agree with the current state of the Items collection. The following differences were detected: Accumulated count 2 is different from actual count 3. [Accumulated count is (Count at last Reset + #Adds - #Removes since last Reset). – SoftwareFactor Sep 09 '13 at 17:23
  • @Nathan Phillips I know I'm like a year late to this thread, but i'm using you MTObservableCollection implementation and it works pretty well. However, rarely, I will get that Index out of range exception intermittently. Do you have any idea why this would happen intermittently? –  Nov 04 '13 at 19:37
  • This work great and save me a lot of troubles. Been using for months and felt like sharing my experience with this. The only little thing i have problem with is that the dispatcher pretty much run whenever he wants so if i query the collection soon after it's occasionally empty or all items are not within the collection yet. Still pretty rare occurrence. I did need a 100% bug free so i made a class that retrieve the collection and that class has a thread sleep of a tenth of a second and error didn't happened since. – Franck Mar 14 '14 at 17:23
  • That's because this solution isn't enough to guarantee thread-safety. Try the following link which provides a thread-safe solution that works from any thread and can be bound to via multiple UI threads : http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection – Anthony Apr 15 '14 at 19:24
  • @SoftwareFactor i get the same error where can i find Jonathan version ? – MonsterMMORPG Aug 13 '14 at 23:48
  • @Franck i suppose i get the same error how can i fix ? TY – MonsterMMORPG Aug 13 '14 at 23:49
  • @Anthony that also gives error :( An ItemsControl is inconsistent with its items source. See the inner exception for more information. / inner exception : System.Exception: Information for developers (use Text Visualizer to read this): This exception was thrown because the generator for control 'System.Windows.Controls.ListBox Items.Count:5' with name 'lstBoxMoreCommonFiredEvents' has received sequence of CollectionChanged events that do not agree with the current state of the Items collection. The following differences were detected: – MonsterMMORPG Aug 14 '14 at 00:04
  • @MonsterMMORPG I left this running with about 10 windows open all night without issue so I'm surprised if you're getting an exception with the collection. Are you sure you are referring to my project and not someone else's? The reason I ask is because my project does not contain any controls named 'lstBoxMoreCommonFiredEvents'. Please send me some further details. – Anthony Aug 14 '14 at 01:19
  • @Anthony i dont run your project of course. I added your classes to my project, and used as public static ObservableImmutableList ocEventsCollection = new ObservableImmutableList(); . But it fails :( I mean sometimes work but gives also error. I am using exactly same way MTObservableCollection and it works better than yours. But it also sometimes gives error. – MonsterMMORPG Aug 14 '14 at 01:21
  • @MonsterMMORPG Send me a zip of your project to AnthonyPaulO at my hotmail account... that's a letter 'O' at the end, not a zero. – Anthony Aug 14 '14 at 01:25
  • @MonsterMMORPG: Jonathan's version is posted in Jonathan's answer for this question :) – SoftwareFactor Aug 14 '14 at 05:03
  • @Anthony sent you email ty. Also your class not supporting Path=[0] while MTObservableCollection supports to get 0 indexed element in the collection – MonsterMMORPG Aug 14 '14 at 12:36
  • @MonsterMMORPG As I explained in the email, you can no longer use the old non-thread-safe methods such as Insert... you must use the new thread-safe methods I introduced such as DoOperation or TryOperation. Using the non-thread-safe methods will undoubtedly result in exceptions when called from different threads so forget about them and use what I provide. As for the binding this was indeed an issue and I fixed it and published the change to The Code Project; once it's approved you should download the new version. Thanks! – Anthony Aug 15 '14 at 00:41
17

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.

Cameron MacFarland
  • 65,569
  • 20
  • 98
  • 130
7

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

Jonathan Allen
  • 63,625
  • 65
  • 234
  • 426
  • 3
    Note that this will cause a thread switch for each collection change and that all changes are serialized (which defeats the purpose of having background threads :-)). For a few items it doesn't matter but if you plan to add many items it will hurt performance a lot. I usually add the items to another collection in the background thread and then move them to the gui collection on a timer. – adrianm Jan 26 '10 at 06:58
  • 1
    I can live with that. The cost I'm trying to avoid is fetching the items in the first place, as it will lock the UI. Adding them to the collection is cheap by comparison. – Jonathan Allen Jan 26 '10 at 07:26
  • @adrianm I am interested in your remark: what do you mean by "serialization" in this case? And do you have an example of "move to the gui collection on a timer"? – Gerard Mar 17 '14 at 09:02
  • All changes to the collection will cause a `dispatcher.Invoke`, i.e. do something on the GUI thread. This means two things: 1. the worker thread has to stop and wait for GUI thread every time it adds something to the collection. Task switching is expensive and will decrease performance. 2. The GUI thread might choke on the amount of work leading to unresponsive GUI. A timer based solution for a similar issue can be found here http://stackoverflow.com/a/4530900/157224. – adrianm Mar 17 '14 at 12:15
3

You can also look at: BindingOperations.EnableCollectionSynchronization.

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

Community
  • 1
  • 1
Richard
  • 31
  • 1
2

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

Community
  • 1
  • 1
norekhov
  • 2,557
  • 18
  • 34
2

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.

Hamish
  • 450
  • 6
  • 15
1

Try This:

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

 //Code

}));
Nalan Madheswaran
  • 8,156
  • 1
  • 48
  • 37
1

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;
            }
        }   

    }
DmitryBoyko
  • 32,983
  • 69
  • 281
  • 458
  • This is a very good solution but there is an error Clark, when you create the instance of the timer, in order for it to work, you need to pass the Application Dispatcher to it! You can do in the constructor by passing, other than the priority, the System.Windows.Application.Current.Dispatcher object! – Andry Jun 12 '13 at 08:33
0

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
Peter pete
  • 574
  • 1
  • 5
  • 15
0

None of them, just use Dispatcher.BeginInvoke

Ana Betts
  • 71,086
  • 16
  • 135
  • 201
  • That defeats the purpose of having background threads and an independent data layer. – Jonathan Allen Jan 26 '10 at 06:17
  • 3
    No it doesn't - all of the work is to fetch the data / process it; you do this in the background thread, then use Dispatcher.BeginInvoke to move it to the collection (which takes very little time hopefully). – Ana Betts Jan 27 '10 at 16:50
0

Small mistake in the VB version. Just replace :

Dim obj As DispatcherObject = invocation.Target

By

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)