Asynchronous MVVM commands
Getting Stephen Cleary's IAsyncCommand pattern working with functions that take a parameter when producing the Task to be executed would require just a few tweaks to his AsyncCommand class and static helper methods.
Starting with his classes found in the AsyncCommand4 sample in the link above, let's modify the constructor to take a function with inputs for a parameter (of type object - this will be the Command Parameter) as well as a CancellationToken and returning a Task. We will also need to make a single change in the ExecuteAsync method so we can pass the parameter into this function when executing the command. I created a class called AsyncCommandEx (shown below) that demonstrates these changes.
public class AsyncCommandEx<TResult> : AsyncCommandBase, INotifyPropertyChanged{ private readonly CancelAsyncCommand _cancelCommand; private readonly Func<object, CancellationToken, Task<TResult>> _command; private NotifyTaskCompletion<TResult> _execution; public AsyncCommandEx(Func<object, CancellationToken, Task<TResult>> command) { _command = command; _cancelCommand = new CancelAsyncCommand(); } public ICommand CancelCommand { get { return _cancelCommand; } } public NotifyTaskCompletion<TResult> Execution { get { return _execution; } private set { _execution = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; public override bool CanExecute(object parameter) { return (Execution == null || Execution.IsCompleted); } public override async Task ExecuteAsync(object parameter) { _cancelCommand.NotifyCommandStarting(); Execution = new NotifyTaskCompletion<TResult>(_command(parameter, _cancelCommand.Token)); RaiseCanExecuteChanged(); await Execution.TaskCompletion; _cancelCommand.NotifyCommandFinished(); RaiseCanExecuteChanged(); } protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } private sealed class CancelAsyncCommand : ICommand { private bool _commandExecuting; private CancellationTokenSource _cts = new CancellationTokenSource(); public CancellationToken Token { get { return _cts.Token; } } bool ICommand.CanExecute(object parameter) { return _commandExecuting && !_cts.IsCancellationRequested; } void ICommand.Execute(object parameter) { _cts.Cancel(); RaiseCanExecuteChanged(); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void NotifyCommandStarting() { _commandExecuting = true; if (!_cts.IsCancellationRequested) return; _cts = new CancellationTokenSource(); RaiseCanExecuteChanged(); } public void NotifyCommandFinished() { _commandExecuting = false; RaiseCanExecuteChanged(); } private void RaiseCanExecuteChanged() { CommandManager.InvalidateRequerySuggested(); } }}
It will also be helpful to update the static AsyncCommand helper class to make the the creation of Command Parameter-aware IAsyncCommands easier. To handle the possible combinations of functions that do or do not take a Command Parameter we will double the number of methods but the result is not too bad:
public static class AsyncCommandEx{ public static AsyncCommandEx<object> Create(Func<Task> command) { return new AsyncCommandEx<object>(async (param,_) => { await command(); return null; }); } public static AsyncCommandEx<object> Create(Func<object, Task> command) { return new AsyncCommandEx<object>(async (param, _) => { await command(param); return null; }); } public static AsyncCommandEx<TResult> Create<TResult>(Func<Task<TResult>> command) { return new AsyncCommandEx<TResult>((param,_) => command()); } public static AsyncCommandEx<TResult> Create<TResult>(Func<object, Task<TResult>> command) { return new AsyncCommandEx<TResult>((param, _) => command(param)); } public static AsyncCommandEx<object> Create(Func<CancellationToken, Task> command) { return new AsyncCommandEx<object>(async (param, token) => { await command(token); return null; }); } public static AsyncCommandEx<object> Create(Func<object, CancellationToken, Task> command) { return new AsyncCommandEx<object>(async (param, token) => { await command(param, token); return null; }); } public static AsyncCommandEx<TResult> Create<TResult>(Func<CancellationToken, Task<TResult>> command) { return new AsyncCommandEx<TResult>(async (param, token) => await command(token)); } public static AsyncCommandEx<TResult> Create<TResult>(Func<object, CancellationToken, Task<TResult>> command) { return new AsyncCommandEx<TResult>(async (param, token) => await command(param, token)); }}
To continue with Stephen Cleary's sample, you can now build an AsyncCommand that takes an object parameter passed in from the Command Parameter (which can be bound to the UI):
CountUrlBytesCommand = AsyncCommandEx.Create((url,token) => MyService.DownloadAndCountBytesAsync(url as string, token));