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. )
CodePudding user response:
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.