Reputation: 2916
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
IsAlive == true
)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.
Upvotes: 2
Views: 722
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
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