1

I have a ViewModel that internally uses a Dispatcher to update an ObservableCollection asynchronously. I would like to write an unit test for that ViewModel, including the method that uses the Dispatcher.

I have abstracted the dispatcher using a custom IDispatcher injected at runtime.

Here is the IDispatcher implementation that I use when the app runs in normal mode

public class WPFDispatcher : IDispatcher
{
    public void Invoke(Action action)
    {
        System.Windows.Application.Current.Dispatcher.Invoke(action);
    }
}

The ViewModel uses the IDispatcher like so

public async Task RefreshControls()
{
    Parent.IsSoftBusy = true;
    if (ControlsList == null)
        ControlsList = new ObservableCollection<DataprepControl>();
    await Task.Run(() =>
    {
        var updatedControls = _getControls();
        Dispatcher.Invoke(() => _handleUpdatedControl(updatedControls));
    });
    Parent.IsSoftBusy = false;
}

When switching to an execution from an unit test (Visual Studio Unit Test Framework), System.Windows.Application` might be null so I manually instantiate it at unit test startup

new System.Windows.Application();

When doing that, Actions passed to Invoke are never executed, they hang indefinitely.

If I add a breakpoint on the Invoke call, I see the Dispatcher.Thread is

  • In STA mode (so it CAN handle GUI updates)
  • Is alive (IsAlive == true)
  • Is in the state Background | WaitSleepJoin (I don't know what that means, but it might be useful)

I do not understand why the actions are not being queued.

Remember that I am in a unit-test context, with no controls, so I cannot directly call the dispatcher and set a timer to it

Adding a static class that somehow tells the Dispatcher to consume its tasks has no effect

Changing to BeginInvoke does not solve the issue.

Arthur Attout
  • 2,701
  • 2
  • 26
  • 49
  • If you inject an `IDispatcher` at runtime, why don't you just inject a special `IDispatcher` for unit tests which executes the action directly? Isn't that the whole point of injecting the `IDispatcher` rather than calling WPF's Dispatcher directly? – Heinzi Sep 01 '21 at 09:19
  • That's exactly what I have done. However, no matter what "special-IDispatcher" implementation I come up with, it systematically halts on the `Invoke`. I forgot to mention also that [adding a static class that somehow tells the Dispatcher to consume its tasks has no effect](https://stackoverflow.com/a/1513399/7540393). – Arthur Attout Sep 01 '21 at 09:26
  • Besides, what do you have in mind when saying "A special `IDispatcher`" ? If I mock it with a class that only executes the action on the current Thread, the ObservableCollection will still complain that it cannot be edited outside of the Thread that has initialized it. – Arthur Attout Sep 01 '21 at 09:27
  • Ah, OK, then I'll remove the duplicate. – Heinzi Sep 01 '21 at 09:28
  • *"the ObservableCollection will still complain that it cannot be edited outside of the Thread that has initialized it."* Good point. I'm afraid, I don't have a solution then, just a few things to try: What happens if you replace the dispatcher call with a task continuation with TaskScheduler.FromCurrentSynchronizationContext()? That should capture the context in which the ObservableCollection was created and execute the continuation in the same context. – Heinzi Sep 01 '21 at 09:37
  • @Heinzi I'm not sure what you mean by that suggestion. Should `TaskScheduler.FromCurrentSynchronizationContext()` have a method `ContinueWith` that I'm missing ? – Arthur Attout Sep 01 '21 at 14:26
  • Okay, I gave it a spin and it still returns that [I cannot do that](https://stackoverflow.com/questions/18331723/this-type-of-collectionview-does-not-support-changes-to-its-sourcecollection-fro). – Arthur Attout Sep 01 '21 at 14:36
  • I see. Thanks for trying, and sorry I couldn't be of more help. – Heinzi Sep 01 '21 at 14:39

2 Answers2

1

Assuming the view model method is called in the UI thread of your application, the following code modification should eliminate the need for using a Dispatcher:

public async Task RefreshControls()
{
    Parent.IsSoftBusy = true;

    if (ControlsList == null)
    {
        ControlsList = new ObservableCollection<DataprepControl>();
    }

    var updatedControls = await Task.Run(() => _getControls());

    _handleUpdatedControl(updatedControls);

    Parent.IsSoftBusy = false;
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • I can validate that moving the instruction that touches the ObservableCollection _outside_ of `Task.Run` did the trick. Now this will require some rework on my end because `_handleUpdatedControls` is super heavy and needs to be ran asynchronously. Thanks for the tip ! – Arthur Attout Sep 01 '21 at 10:48
  • But Dispatcher.Invoke would also have called it in the UI thread. It shouldn't make any difference. – Clemens Sep 01 '21 at 10:55
  • This is true. I don't know what I had in mind. I just realized this whole question was a misuse of `Dispatcher.Invoke` inside a `Thread.Run` which, as you said, should theoretically not make any difference, but hangs when executed in a unit test. – Arthur Attout Sep 01 '21 at 11:02
  • Correction, I remember why I did that. I did not know that `Task.Run` could return a concrete result. I thought it was something awaitable but with some built-in restrictions. So I thought I couldn't run `__getControls` asynchronously AND do something with its result synchronously. – Arthur Attout Sep 01 '21 at 11:05
0

As the other answer stated, it's always best to use await for updating the UI with results of a background operation, or if you need multiple updates, use IProgress<T>.

There are some scenarios where this isn't possible, though, such as external factors needing to update your UI. In this case you have some update coming in on a background thread and you do need to queue an update to the UI.

In this scenario, I generally recommend wrapping SynchronizationContext instead of Dispatcher. The API is more awkward but it's more portable: SynchronizationContext works on all UI platforms. If you wrap SynchronizationContext, then you can use AsyncContext for testing.

If you do need an actual Dispatcher, though, then creating a Dispatcher instance is insufficient:

Dispatcher.Thread is In STA mode (so it CAN handle GUI updates)

The thread is marked for STA, but part of actually being an STA thread is that it must have a message loop. So, you'd need an actual STA thread (i.e., one that does message pumping) for unit tests that queue to an STA. For testing in this scenario you should use something like WpfContext.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810