Reputation: 4916
I have been following the rather excellent series of articles by Stephen Cleary in the MSDN magazine (Patterns for Asynchronous MVVM Applications) and have been using his IAsyncCommand
pattern in a "hello world" style application.
However, one area that he does not address is when one needs to pass in a Command Parameter (using this pattern). For a trivial example, take Authentication where the Password control may not be data-bound for security reasons.
I wonder if anyone had managed to get his AsyncCommand
to work with parameters, and if so, would they share their findings?
Upvotes: 19
Views: 10180
Reputation: 1696
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));
Upvotes: 18