Asynchronous MVVM commands Asynchronous MVVM commands wpf wpf

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));