Arthur Attout
Arthur Attout

Reputation: 2916

WPF dispatcher never executes action when running UI tests

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

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.

Upvotes: 2

Views: 722

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456437

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.

Upvotes: 0

Clemens
Clemens

Reputation: 128060

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;
}

Upvotes: 1

Related Questions