WPF like WinForms has a UI thread and enforces that UI properties cannot be modified by threads other than the UI thread. There are many reasons for this design choice, but the reality is that if you want a responsive user interface you will need to make sure that any significant work is performed upon a background thread. What this post is about, is some of the ways you can use built in WPF patterns to develop an properly asynchronous UI, and some areas that need extension.
Data-binding is one area that WPF provides built in asynchronous patterns. Your first friend for data-binding is ObjectDataProvider. This nifty class sometimes confuses developers who wonder why it is necessary if WPF can bind to any data object because some examples show it wrapping an object. But where ObjectDataProvider shines is in providing an asynchronously loaded object to WPF binding's. For example, if your UI needed to bind to properties of the system's NetworkInterface objects, you could do something similar to:
<Page ...
xmlns:NetworkInformation="clr-namespace:System.Net.NetworkInformation;assembly=System" ...>
...<Page.Resources>
<ObjectDataProvider
x:Key="_interfaces"
IsAsynchronous="True"
ObjectType="NetworkInformation:NetworkInterface"
MethodName="GetAllNetworkInterfaces"/>
</Page.Resources>
This succinct little bit will call GetAllNetworkInterfaces asynchronously. In addition, any parts of your UI that create bindings to _interfaces (through {Binding Source={StaticResource _interfaces} ...}) will initially appear empty (actually it will display the binding's FallbackValue) but update after the asynchronous operation completes.
WPF also provides a property to every binding called IsAsync. Hopefully you will have less use for this property than ObjectDataProvider because property access should not be slow. I also prefer that data load all at once, or at least in chunks.
Surprisingly, after providing good patterns for Binding, WPF seems to neglect the more common need of asynchronous commands. To be fair, they are less simple because it's fairly common for them to be dependent on the current setting of UI properties. When commands are not dependent upon UI properties they can be very simple. In many ways, this is the optimal usage for commands in general because the whole CommandParameter system is a bit clunky.
WPF does support BackgroundWorker, which can be used to create a basic AsyncCommand.
public abstract class AsyncCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public event EventHandler RunWorkerStarting;
public event RunWorkerCompletedEventHandler RunWorkerCompleted;
public abstract string Text { get; }
private bool _isExecuting;
public bool IsExecuting
{
get { return _isExecuting; }
private set
{
_isExecuting = value;
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
protected abstract void OnExecute(object parameter);
public void Execute(object parameter)
{
try
{
onRunWorkerStarting();
var worker = new BackgroundWorker();
worker.DoWork += ((sender, e) => OnExecute(e.Argument));
worker.RunWorkerCompleted += ((sender, e) => onRunWorkerCompleted(e));
worker.RunWorkerAsync(parameter);
}
catch (Exception ex)
{
onRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
}
}
private void onRunWorkerStarting()
{
IsExecuting = true;
if (RunWorkerStarting != null)
RunWorkerStarting(this, EventArgs.Empty);
}
private void onRunWorkerCompleted(RunWorkerCompletedEventArgs e)
{
IsExecuting = false;
if (RunWorkerCompleted != null)
RunWorkerCompleted(this, e);
}
public virtual bool CanExecute(object parameter)
{
return !IsExecuting;
}
}
From this point, you can override CanExecute and OnExectute to provide the actual body of your command. While an asynchronous operation is executing the button/checkbox (any inheritor of ButtonBase) will be disabled.
If you need to pass a parameter in you can do so with CommandParameter, but one caution is in order. There is some advice that for passing multiple to CommandParameter you should pass a complex object in an instance field, or resource that parts of your form bind to. While this works fine for synchronous invocation it could be problematic for asynchronous because the bindings can alter the object while your asynchronous code is executing.
What I do when I need to pass in multiple values is to use a IMultiValueConverter and MultiBinding.
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource InstallServiceParameterConverter}">
<MultiBinding.Bindings>
<Binding ElementName="_this" Path="IsInstalled"/>
<Binding ElementName="localURI" Path="Text"/>
<Binding ElementName="meshURI" Path="Text"/>
<Binding ElementName="registerWithMesh" Path="IsChecked"/>
</MultiBinding.Bindings>
</MultiBinding>
</Button.CommandParameter>
It is a bit clunky, especially with the converter, but it's also safe since your Command will be passed a newly created object. I'm still looking for better solutions, but so far this seems the closest to ideal.
0 comments:
Post a Comment