0

Consider the unit test below :

[TestMethod]
public void MVCE()
{
    asyncMethodNoCollectionView().Wait(); // Does not fail
    asyncMethodWithCollectionView().Wait(); // Fails
}

private async Task asyncMethodNoCollectionView()
{
    var collection = new ObservableCollection<string>();
    await Task.Run(() =>
    {
        Task.Delay(4000).Wait();
    });
    collection.Add("hello");
}

private async Task asyncMethodWithCollectionView()
{
    var collection = new ObservableCollection<string>();
    var view = CollectionViewSource.GetDefaultView(collection);
    await Task.Run(() =>
    {
        Task.Delay(4000).Wait();
    });
    collection.Add("hello");
}

For some obscure reason I cannot figure out, calling CollectionViewSource.GetDefaultView(collection); will make the .Add("Hello") line in the second method fail, returning a :

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

And this behaviour is consistent (I ran the test dozens of times, on separate machines, with different workload).

My understanding is that the exception is thrown because the Thread resuming the await in the 1st method is the one that created the ObservableCollection. Whereas in the 2nd method, the resuming Thread is not the one that created the collection. Hence, it fails because this collection must be edited on the original Thread.

Why would calling CollectionViewSource.GetDefaultView cause the resuming thread to systematically be different from the originating one ?

(This question is somehow linked, but it is not clear to me how it answers mine. )

Arthur Attout
  • 2,701
  • 2
  • 26
  • 49
  • `cause the resuming thread` it doesn't. Tasks are processed by threadpool threads. Execution resumes after `await` in the original synchronization context, but console applications *don't* have a synchronization context. – Panagiotis Kanavos Sep 21 '22 at 11:08
  • There's no reason to use `await Task.Run` just to call `Task.Delay().Wait()`. Use `await Task.Delay()` instead – Panagiotis Kanavos Sep 21 '22 at 11:12
  • Indeed, I left this as-is because there was something similar in my original code, which I simplified as much as possible to narrow down the issue. – Arthur Attout Sep 21 '22 at 11:24
  • Try removing all such code and change the test response types to `async Task`, and actually await the async methods. I haven't used MSTest in ages, but I doubt *any* test runner would create a sync context for a synchronous method – Panagiotis Kanavos Sep 21 '22 at 11:27
  • Ok, I gave this a try, changing the main test return type to `async Task` and `await`ing the methods still threw the exception. – Arthur Attout Sep 21 '22 at 11:42
  • 1
    You could try explicitly setting the synchronization context with `SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext())`. Or use a different test framework like xUnit. xUnit *does* have a synchronization context while MSTest doesn't – Panagiotis Kanavos Sep 21 '22 at 12:05

1 Answers1

2

It's not CollectionViewSource.GetDefaultView(collection) that causes the switch.

await awaits an asynchronous operation to complete and then resumes execution in the original synchronization context. In desktop applications, that synchronization context is the UI thread. In console applications there is no synchronization context and execution resumes on a threadpool thread.

Test runners are console applications and have no synchronization context unless the runner itself provides one. The question's test methods aren't asynchronous though, so I doubt MSTest would create one.

Both CollectionView and CollectionViewSource are UI elements, so they can only be modified by the same thread that created them (typically the UI thread). More specifically, both inherit from DispatcherObject which

Represents an object that is associated with a Dispatcher.

And as the docs warn :

Only the thread that the Dispatcher was created on may access the DispatcherObject directly. To access a DispatcherObject from a thread other than the thread the DispatcherObject was created on, call Invoke or BeginInvoke on the Dispatcher the DispatcherObject is associated with.

The exception stack trace is missing but I suspect it would show the exception starts inside CollectionView. That class listens to ObservableCollection events and modifies itself. When the second test modifies the ObservableCollection, the View will try to modify itself, find out it's in the wrong thread and throw.

The first test works because ObservableCollection is not a UI element. It's a collection that raises a specific event when modified.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • _The first test works because ObservableCollection is not a UI element. It's a collection that raises a specific event when modified._ and isn't that true as well for the second test ? – Arthur Attout Sep 21 '22 at 11:23
  • No, these are UI elements. Both classes inherit from DispatcherObject. – Panagiotis Kanavos Sep 21 '22 at 11:24
  • Should I conclude that `CollectionViewSource.GetDefaultView(collection)` *transforms* the ObservableCollection into a UI element ? Does the method mutate the inner element ? – Arthur Attout Sep 21 '22 at 11:26
  • I should stress that the return value of `CollectionViewSource.GetDefaultView(collection)` is discarded, and I do not manipulate it. The object on which I call `.Add` is always the ObservableCollection, in both methods. – Arthur Attout Sep 21 '22 at 11:27
  • 1
    No, it's *CollectionView* that throws. You didn't post the full exception string, but I suspect you'll see the exception started somewhere inside CollectionView. When ObservableCollection is modified it raises an event that's handled by CollectionView. When CollectionView tries to modify itself it finds out is on the wrong thread and throws – Panagiotis Kanavos Sep 21 '22 at 11:28
  • Indeed, the exception was from `CollectionView` (even though it triggered only at the `.Add`). It's not exactly clear to me how I should handle this scenario, but I'll look into test frameworks that handle context synchronisation with a bit more fine-grain than MSTest. – Arthur Attout Sep 22 '22 at 08:29