Wednesday, June 19, 2013

MVVM and Accessing the UI Thread in Windows Store Apps

Any developer who has worked with MVVM in WPF, Silverlight, or Windows Store apps has likely run into the issue of executing an action on the UI thread. This most often happens when work has to be done on another thread. It is a common mistake to perform the work and then attempt to set a property on the view model. The view model exposes properties for data-binding, and data-binding must happen on the UI thread. Therefore, triggering a property change notification on the wrong thread can lead to a thread access violation and result in a crash. It is also necessary to access the UI thread when performing UI-related actions such as showing a modal dialog and waiting for user input.

I’ve seen a lot of different approaches to doing this, some of them quite complex and interesting to read, but a simple solution is easier than you might think. You can choose to set a property on the view model that references the UI thread:

private readonly CoreDispatcher dispatcher;

The dispatcher class queues messages for publication on the UI thread. The messages are callbacks of type DispatchHandler which is simply a named delegate. The dispatcher allows you to specify a priority for the message and higher priority messages will be queued in front of lower priority messages. I’ve rarely found the need to use anything other than the normal priority when performing work on the UI thread.

In this example I’m using a simple property to capture the dispatcher, but you may want to consider creating an interface that mimics the work the dispatcher does. Then you can implement a reference to the dispatcher at runtime but use a mocked implementation for tests.

Grabbing the right dispatcher is easier than you might think. I’ve seen solutions that use dependency properties and data-binding or wire up the dispatcher using code-behind, etc. but the reality is the view model is almost always instantiated on the UI thread (this is true whether it is created in code behind, referenced as a resource or instantiated directly from XAML). Therefore, you can simply capture the dispatcher in the constructor. Be sure you check for design-mode as the attempt to capture the dispatcher will fail in the designer.

if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
{
    return;
}
this
.dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;

Now that I have captured the dispatcher, I can create a simple method that encapsulates the call to the UI thread (in this case I’m assuming it’s on the view model, but again this could be in an implementation that is referenced via an interface to make it easier to mock for testing).

private async Task OnUiThread(Action action)
{
    await this.dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action());
}

This is a simple form – it takes an action and simply waits for the dispatcher to queue the callback on the UI thread. The following example will queue the assignment of a property on the UI thread so that data-binding can process the update (note there is another technique with view models that involves always marshalling the property change event to the UI thread). The following code is executed from a background task that is not running on the UI thread:

await this.OnUiThread(() =>
{
this.ViewModelProperty = this.BackgroundWork();
});

This will work for most cases, but if you are scheduling something asynchronous you’ll have a problem. The method I showed performs a “fire and forget” of the work you pass to it. If you are doing something like waiting for a dialog, your code may not behave as intended because it won’t actually wait for the dialog to close – instead, it will simply wait for the call to open the dialog to be scheduled. To solve this, provide a simple overload:

private async Task OnUiThread(Func<Task> action)
{
    await this.dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => await action());
}

The overload also schedules a task, but it also acknowledges the task is asynchronous and awaits the completion of the task. Now you can schedule the work like this:

var dialog = new MessageDialog(message, title);
await this.OnUiThread(async () => await dialog.ShowAsync());

One important thing to keep in mind is that this should be the exception, not the rule, for most apps. A good design will not attempt to manipulate the view model directly, but instead will return a property and allow you to await the task from the UI thread. For example, setting the view model property might be accomplished like this without having to use the dispatcher at all:

this.ViewModelProperty = await this.BackgroundWorkAsync();                       

The dispatcher is most useful when you have scenarios that force you to spawn background tasks that you aren’t able to await from the UI thread, or when you have methods that may be called from a separate thread and must surface information to the UI.

No comments:

Post a Comment