In a previous post I showed how to use SignalR as one option for providing feedback to the client application during a long running service operation. However, I didn’t display this on the UI because that would have made the blog post long and complicated as I tried to explain propagating a change from a non-UI thread, back onto the UI thread so it could be displayed within the app. In this post I’m going to do just that – what’s interesting is that each platform handles this scenario slightly different with some caring about thread affinity, whilst others do not.
Firstly, I’ll update the layout to include a TextBlock that’s going to display the progress:
<TextBlock Text=”{Binding Progress}” />
In this case databinding to the Progress property on the MainViewModel:
private string progress;
public string Progress
{
get { return progress; }
set
{
if (Progress == value) return;
progress = value;
OnPropertyChanged();
}
}
Note that as this property changes it calls the OnPropertyChanged method which will be used to raise the PropertyChanged event specified in the INotifyPropertyChanged interface – this is what the XAML data binding framework uses to detect when bound properties are changed. In this case I’m going to implement this interface in BaseViewModel, and have MainViewModel inherit from BaseViewModel, rather than implement it in every view model.
public class MainViewModel:BaseViewModel { … }
public class BaseViewModel:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));}
}
Now, let’s modify the delegate that gets invoked when we receive an update from the service. Previously we just had Debug.WriteLine(msg), let’s change that to Progress=msg. When you run this in your Universal Windows application – BANG – Exception due to an attempt to update the UI (ie the Text attribute on the TextBlock) from a non-UI thread. Interestingly doing the same thing in the WPF application doesn’t throw the same exception. In order to update the Progress property, we first need to jump back onto the UI thread, which is fine, if we were working directly with the Page/View. However, we don’t have any notion of a Dispatcher, UI threads etc from within the MainViewModel. This sounds like another scenario for a dependency injected implementation, so here goes:
My BaseViewModel is extended to include a UIContext object:
private readonly UIContext context=new UIContext();
public UIContext UIContext
{
get { return context; }
}
The UIContext object wraps and abstracts away the loading of an implementation of IUIContext:
public interface IUIContext
{
Task RunOnUIThreadAsync(Func<Task> action);
}public class UIContext
{
private IUIContext runContext;
private IUIContext RunContext
{
get
{
if (runContext == null)
{
runContext = ServiceLocator.Current.GetInstance<IUIContext>();}
return runContext;
}
}public async Task RunAsync(Action action)
{
await RunAsync(async () => action());
}public async Task RunAsync(Func<Task> action)
{
var context = RunContext;
await context.RunOnUIThreadAsync(action);
}
}
Of course, we now need an implementation of IUIContext for our Universal Windows application:
public class UniversalUIContext : IUIContext
{
public async Task RunOnUIThreadAsync(Func<Task> action)
{
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,async () => await action());
}
}
And this needs to be registered with Autofac in the ClientApplicationCore – note the use of the NETFX_CORE compilation attribute.
CoreApplication.Startup(builder =>
{
builder.RegisterType<SignalRFactory>().As<ISignalR>();
#if NETFX_CORE
builder.RegisterType<UniversalUIContext>().As<IUIContext>();
#endif
});
And finally I need to go back to my initial progress statement and update it from msg => Progress = msg, to the following:
async msg =>
await UIContext.RunAsync(() =>
{
Progress = msg;
})
This issue isn’t specific to Universal applications, and you should always make sure you have access to the UI thread when making UI changes (such as updating data bound properties).