Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asynchronous MVVM commands

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?

like image 751
DrGriff Avatar asked Jun 09 '15 19:06

DrGriff


1 Answers

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));
like image 88
C8H10N4O2 Avatar answered Oct 06 '22 05:10

C8H10N4O2