Where do I get a thread-safe CollectionView?
Asked Answered
F

12

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?

Foretopmast answered 26/1, 2010 at 6:12 Comment(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 : codeproject.com/Articles/64936/…Contort
C
65

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.

Carnay answered 4/9, 2012 at 0:32 Comment(15)
Bit late and an old topic but this bit of code has saved me alot of headaches, thanks! :)Shwalb
Caliburn also has a really nice implementation in their BindableCollection<T>. Take a look here: caliburn.codeplex.com/SourceControl/changeset/view/…Hearing
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).Grenadines
@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?Lassie
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.Educationist
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 : codeproject.com/Articles/64936/…Contort
@Grenadines i get the same error where can i find Jonathan version ?Jodhpurs
@Educationist i suppose i get the same error how can i fix ? TYJodhpurs
@Contort 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:Jodhpurs
@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.Contort
@Contort i dont run your project of course. I added your classes to my project, and used as public static ObservableImmutableList<string> ocEventsCollection = new ObservableImmutableList<string>(); . 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.Jodhpurs
@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.Contort
@MonsterMMORPG: Jonathan's version is posted in Jonathan's answer for this question :)Grenadines
@Contort sent you email ty. Also your class not supporting Path=[0] while MTObservableCollection supports to get 0 indexed element in the collectionJodhpurs
@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!Contort
P
89

Use:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    (Action)delegate() 
    {
         // Your Action Code
    });
Pollack answered 16/12, 2010 at 14:18 Comment(3)
Simple, Elegant, Straight to the point, love it.... Learned it for good. Thanks.Splendid
using Invoke results in UI freezing. Use BeginInvoke instead.Drag
@MonsterMMORPG This solution with .BeginInvoke instead .Invoke is good answer.Navaho
C
65

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.

Carnay answered 4/9, 2012 at 0:32 Comment(15)
Bit late and an old topic but this bit of code has saved me alot of headaches, thanks! :)Shwalb
Caliburn also has a really nice implementation in their BindableCollection<T>. Take a look here: caliburn.codeplex.com/SourceControl/changeset/view/…Hearing
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).Grenadines
@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?Lassie
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.Educationist
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 : codeproject.com/Articles/64936/…Contort
@Grenadines i get the same error where can i find Jonathan version ?Jodhpurs
@Educationist i suppose i get the same error how can i fix ? TYJodhpurs
@Contort 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:Jodhpurs
@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.Contort
@Contort i dont run your project of course. I added your classes to my project, and used as public static ObservableImmutableList<string> ocEventsCollection = new ObservableImmutableList<string>(); . 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.Jodhpurs
@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.Contort
@MonsterMMORPG: Jonathan's version is posted in Jonathan's answer for this question :)Grenadines
@Contort sent you email ty. Also your class not supporting Path=[0] while MTObservableCollection supports to get 0 indexed element in the collectionJodhpurs
@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!Contort
C
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.

Credent answered 26/1, 2010 at 7:4 Comment(1)
I like mark's implementation better, but I have to give you the credit for finding the best explaination.Foretopmast
F
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

Foretopmast answered 26/1, 2010 at 6:48 Comment(4)
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.Devon
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.Foretopmast
@Devon 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"?Overtire
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 https://mcmap.net/q/282051/-how-to-do-the-processing-and-keep-gui-refreshed-using-databinding.Devon
S
3

You can also look at: BindingOperations.EnableCollectionSynchronization.

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

Spatiotemporal answered 24/6, 2013 at 12:23 Comment(0)
L
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

Lovegrass answered 3/4, 2014 at 14:0 Comment(0)
C
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.

Crack answered 12/8, 2019 at 5:32 Comment(0)
S
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;
            }
        }   

    }
Slew answered 26/1, 2012 at 16:29 Comment(1)
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!Adamant
L
1

Try This:

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

 //Code

}));
Lindalindahl answered 14/3, 2013 at 11:57 Comment(0)
F
0

None of them, just use Dispatcher.BeginInvoke

Fact answered 26/1, 2010 at 6:15 Comment(2)
That defeats the purpose of having background threads and an independent data layer.Foretopmast
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).Fact
V
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: https://mcmap.net/q/280313/-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
Vanir answered 17/12, 2013 at 10:4 Comment(0)
T
0

Small mistake in the VB version. Just replace :

Dim obj As DispatcherObject = invocation.Target

By

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)
Treacle answered 18/8, 2016 at 9:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.