Асинхронные команды MVVM

Я следил за довольно прекрасной серией статей Стивена Клири в журнале MSDN (Шаблоны для асинхронных приложений MVVM) и использовали его шаблон IAsyncCommand в приложении стиля "привет мир".

Однако одна область, которую он не адресует, - это когда нужно передать в Параметр команды (используя этот шаблон). Для тривиального примера возьмите Authentication, где элемент управления паролем может не привязываться к данным по соображениям безопасности.

Интересно, сумел ли кто-нибудь заставить его AsyncCommand работать с параметрами, и если да, разделили бы они свои выводы?

Ответ 1

Получение шаблона Stephen Cleary IAsyncCommand, работающего с функциями, которые принимают параметр при выполнении задачи, подлежащей выполнению, потребует всего лишь нескольких настроек его класса AsyncCommand и статических вспомогательных методов.

Начиная с его классов, найденных в примере AsyncCommand4 в приведенной выше ссылке, позвольте модифицировать конструктор для выполнения функции со входами для параметра (объекта типа - это будет параметр команды), а также CancellationToken и возврата Задача. Мы также должны сделать одно изменение в методе ExecuteAsync, чтобы мы могли передать этот параметр в эту функцию при выполнении команды. Я создал класс AsyncCommandEx (показан ниже), который демонстрирует эти изменения.

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

Также полезно обновить статический вспомогательный класс AsyncCommand, чтобы упростить создание IAsyncCommands, поддерживающих параметры команды. Чтобы обрабатывать возможные комбинации функций, которые выполняют или не принимают командный параметр, мы удвоим количество методов, но результат не так уж плох:

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

Чтобы продолжить с образца Stephen Cleary, теперь вы можете построить AsyncCommand, который принимает параметр объекта, переданный из параметра команды (который может быть привязан к пользовательскому интерфейсу):

CountUrlBytesCommand = AsyncCommandEx.Create((url,token) => MyService.DownloadAndCountBytesAsync(url as string, token));