1

I have an application where items being added to collections from multiple threads. Randomly i get an

This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread. at System.Windows.Data.CollectionView.OnCollectionChanged(Object sender, NotifyCollectionChangedEventArgs args)

Now the collections are created in a class , the classes themselves are created on multiple threads.

Here is a class example

public class Example
{
    public Example()
    {
        BindingOperations.EnableCollectionSynchronization(collection, COLLECTION_LOCK);

        var defaultView = CollectionViewSource.GetDefaultView(collection);
        defaultView.SortDescriptions.Add(new SortDescription("SomeProperty", ListSortDirection.Ascending));

        if (defaultView is ICollectionViewLiveShaping liveShaping)
            liveShaping.IsLiveSorting = true;
    }

    private readonly object COLLECTION_LOCK = new object();
    private readonly ObservableCollection<object> collection = new ObservableCollection<object>();

    public ObservableCollection<object> Collection
    {
        get
        {
            return collection;
        }
    }

    private void AddItem(object item)
    {
        lock(COLLECTION_LOCK)
        {
            if(!Collection.Contains(item))
            {
                Collection.Add(item);
            }
        }
    }

    private void RemoveItem(object item)
    {
        lock (COLLECTION_LOCK)
        {
            if (Collection.Contains(item))
            {
                Collection.Remove(item);
            }
        }
    }
}

I am using the BindingOperations.EnableCollectionSynchronization to allow cross thread operations and always use the specified lock to modify collection. Still the error comes up randomly.

I have also tried to use BindingOperations.AccessCollection when accessing the collection but the error still happens randomly.

The MS documentation states that ObservableCollection must be created on a UI thread? Can someone confirm that its the case?

Also you can notice that i get the default collection view CollectionViewSource.GetDefaultView(collection)

The collection view is also created on same thread and technically as i understand its the source of the problem.

I have tried to simulate adding from different threads by creating thousands of tasks and modifying the collection with no error happening BUT again randomly error pops up out of nowhere, i tested with both where collection was not bound and bound to UI.

Any ideas?

Stack trace

System.NotSupportedException: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.
   at System.Windows.Data.CollectionView.OnCollectionChanged(Object sender, NotifyCollectionChangedEventArgs args)
   at System.Collections.Specialized.NotifyCollectionChangedEventHandler.Invoke(Object sender, NotifyCollectionChangedEventArgs e)
   at System.Collections.ObjectModel.ObservableCollection`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e)
   at System.Collections.ObjectModel.ObservableCollection`1.RemoveItem(Int32 index)
   at System.Collections.ObjectModel.Collection`1.Remove(T item)
   at Manager.ViewModels.HostViewModelBase.RemoveUser(IUserMemberViewModel user)

The collection view flags are System.Windows.Data.CollectionView.CollectionViewFlags.ShouldProcessCollectionChanged | System.Windows.Data.CollectionView.CollectionViewFlags.IsCurrentBeforeFirst | System.Windows.Data.CollectionView.CollectionViewFlags.IsCurrentAfterLast | System.Windows.Data.CollectionView.CollectionViewFlags.IsDynamic | System.Windows.Data.CollectionView.CollectionViewFlags.AllowsCrossThreadChanges | System.Windows.Data.CollectionView.CollectionViewFlags.CachedIsEmpty

and AllowsCrossThreadChanges is true

NullReference
  • 862
  • 1
  • 11
  • 27
  • 1
    You should use `Dispatcher.Invoke` to update the collection. Your question is duplicated with this [one](https://stackoverflow.com/questions/18331723/this-type-of-collectionview-does-not-support-changes-to-its-sourcecollection-fro) – Pavel Anikhouski Apr 12 '19 at 09:59
  • 1
    Possible duplicate of [This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread](https://stackoverflow.com/questions/18331723/this-type-of-collectionview-does-not-support-changes-to-its-sourcecollection-fro) – Jonatan Dragon Apr 12 '19 at 10:04
  • Dispatcher.Invoke ? If it is so why there an api for the Cross Thread modifications ? Also the code itself that makes modifications or creates the Class that contains the collection has no idea of Dispatcher and i dont want to rely on it. – NullReference Apr 12 '19 at 10:06
  • I had the same issue once, you can not alter the Collection from a different thread. It should be modified using the thread that created it. – Smaiil Apr 12 '19 at 10:08
  • @NullReference I had the same problem never knew why? – Avinash Reddy Apr 12 '19 at 10:10
  • Could you get the full callstack when the exception happens? (for that you will need to disable "just my code" in the Visual Studio debugging options, and configure the exception filters to break on first-chance exceptions: https://devblogs.microsoft.com/devops/understanding-exceptions-while-debugging-with-visual-studio/ ) – Kevin Gosse Apr 12 '19 at 10:24
  • @KevinGosse added an edit with stack trace – NullReference Apr 12 '19 at 11:21
  • The implementation of `OnCollectionChanged` is fairly straightforward: https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Data/CollectionView.cs,1177 It's likely that `AllowsCrossThreadChanges` is set to false, and this flag is supposed to be set by `BindingOperations.EnableCollectionSynchronization`. Are you 100% certain that you called it with the same collection as the one bound to your control? – Kevin Gosse Apr 12 '19 at 11:32
  • To be extra sure, could you check the value of `_flags` on your `CollectionView`? (When the exception happens, open the quickwatch window, then type `this._flags`) – Kevin Gosse Apr 12 '19 at 11:34
  • ok I'm stumped. If `AllowsCrossThreadChanges` is true, I don't see how the exception could be thrown at that place. Are you checking at the moment when the exception is thrown, or just when creating the CollectionView? – Kevin Gosse Apr 12 '19 at 11:56
  • I updated the question. As i said the collection is created when class is created. And in constructor i call var defaultView = CollectionViewSource.GetDefaultView(collection); to get default collection view. The collection view cannot be bound to any UI as the class itself is not created yet. – NullReference Apr 12 '19 at 11:58
  • @KevinGosse i checked right after CollectionViewSource.GetDefaultView not at the moment of exception. – NullReference Apr 12 '19 at 11:59
  • @NullReference Please check at the moment of the exception. Make sure to use `this` in the quickwatch window to be sure you're checking the instance of CollectionView that is throwing the exception – Kevin Gosse Apr 12 '19 at 12:02
  • Basically i dont hold any reference to the collectionview just the observable collection itself. The only way i could get the view would be calling CollectionViewSource.GetDefaultView again on the collection being modified. This also brings a question, would it be possible that default collection view is disposed at some point and then recreated by binding? That would explain the error. – NullReference Apr 12 '19 at 12:28
  • @NullReference When the exception is thrown, the CollectionView is on the stack. You don't need a reference to access it from the debugger. As long as you break on the `System.Windows.Data.CollectionView.OnCollectionChanged` method (which should happen if you break on first-chance exceptions), you can access the CollectionView by typing `this` in the quickwatch window – Kevin Gosse Apr 12 '19 at 12:31
  • Also, to solve the problem itself, I think you should create your instances of `Example` from a single thread, or protect the call with a lock. The implementation of `CollectionViewSource.GetDefaultView` and `BindingOperations.EnableCollectionSynchronization` don't look threadsafe to me – Kevin Gosse Apr 12 '19 at 12:32

2 Answers2

3

One of the best ways to deal with that is to ditch ObservableCollection alltoghether. Its use case is very narrow and it's hard to work around the Dispatcher issue.

Go with DynamicData instead - once you get the hang of it, it becomes very powerfull and so natural to use:

ReadOnlyObservableCollection<TradeProxy> data;
var source = new SourceCollection<YourClass>();

source.Connect()
    .Sort(SortExpressionComparer<YourClass>.Descending(t => t.SomeProperty)) 
    .ObserveOnDispatcher()          //ensure operation is on the UI thread
    .Bind(out data)         //Populate the observable collection
    .Subscribe();

// you can do that in ANY THREAD you want and the view will update without any problems:

source.Add(yourClasse);

DynamicData also has filtering with very easy reaplying of the filter, paging, grouping,.... so many things. It is based on Rx, so on top of that you can easily throtle the change when working with big sets, and then make it all instant in UnitTests.

Krzysztof Skowronek
  • 2,796
  • 1
  • 13
  • 29
0

How about implementing a thread safe wrapper of the ObservableCollection?

public class ObservableCollectionWrapper<T> : ICollection<T>, INotifyCollectionChanged
{
    private readonly ObservableCollection<T> _collection;
    private readonly Dispatcher _dispatcher;

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    public ObservableCollectionWrapper(ObservableCollection<T> collection, Dispatcher dispatcher)
    {
        _collection = collection;
        _dispatcher = dispatcher;
        collection.CollectionChanged += Internal_CollectionChanged;
    }

    private void Internal_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        _dispatcher.Invoke(() =>
        {
            this.CollectionChanged?.Invoke(sender, e);
        });
    }

    public int Count => _collection.Count;
    /* Implement the rest of the ICollection<T> interface */
}

Usage example:

var collectionWrapper = new ObservableCollectionWrapper<object>(collection, this.Dispatcher);
var defaultView = CollectionViewSource.GetDefaultView(collectionWrapper);
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104