MVVM - 使用 async 命令进行异步

VVM(模型视图视图模型)模式是使用 XAML 开发 UI 应用程序的事实标准。它还广泛用于使用本机 UI 方法而不是 Xamarin 表单的 Xamarin 应用程序中。

无论我们在其中使用什么 UI 堆栈,总有一个命令的概念。一个命令,实现了自 .NET 3.0 以来一直存在的ICommand接口。

这个概念和它的第一个实现早于异步等待的创建。

在大多数应用程序中,我们会有这样的代码:

public class MainViewModel : ViewModelBase
{
    private bool _isBusy;
    public bool IsBusy
    {
        get => _isBusy;
        private set => Set(ref _isBusy, value);
    }

    public RelayCommand Submit { get; private set; }    

    public MainViewModel()
    {
        Submit = new RelayCommand(ExecuteSubmit, CanExecuteSubmit);
    }

    private void ExecuteSubmit()
    {
        IsBusy = true;
        // Do something
        IsBusy = false;
    }

    private bool CanExecuteSubmit()
    {
        return true;
    }
}

这里我使用的是MvvmLight工具包,所以实现 ICommand 的类是RelayCommand。其他框架可能有其他名称,例如DelegateCommandActionCommand

异步永久链接

正如我们在上面的代码中看到的,在ExecuteSubmit方法中运行异步代码会迫使我们将其标记为异步,这是一个非常糟糕的主意。

如果您不知道为什么async void是个坏主意,请参阅上一篇文章。

介绍 AsyncCommand永久链接

如果您只是对在项目中使用的可重用代码感兴趣,请查看受本文内容启发的AsyncAwaitBestPratices库。

我们不必强制使用 RelayCommand,我们可以制作自己的 ICommand 实现:

public interface IAsyncCommand : ICommand
{
    Task ExecuteAsync();
    bool CanExecute();
}

public class AsyncCommand : IAsyncCommand
{
    public event EventHandler CanExecuteChanged;

    private bool _isExecuting;
    private readonly Func _execute;
    private readonly Func _canExecute;
    private readonly IErrorHandler _errorHandler;

    public AsyncCommand(
        Func execute,
        Func canExecute = null,
        IErrorHandler errorHandler = null)
    {
        _execute = execute;
        _canExecute = canExecute;
        _errorHandler = errorHandler;
    }

    public bool CanExecute()
    {
        return !_isExecuting && (_canExecute?.Invoke() ?? true);
    }

    public async Task ExecuteAsync()
    {
        if (CanExecute())
        {
            try
            {
                _isExecuting = true;
                await _execute();
            }
            finally
            {
                _isExecuting = false;
            }
        }

        RaiseCanExecuteChanged();
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

#region Explicit implementations
    bool ICommand.CanExecute(object parameter)
    {
        return CanExecute();
    }

    void ICommand.Execute(object parameter)
    {
        ExecuteAsync().FireAndForgetSafeAsync(_errorHandler);
    }
#endregion
}

这个实现有一些关键特性:

  • 它不允许并发执行以通过猴子测试。
  • 它提供了显式实现以便与 XAML 绑定。
  • 它使用上一篇文章FireAndForgetSafeAsync中介绍的扩展方法和IErrorHandler接口来处理使用 XAML 绑定时的 async void 问题。
  • 它仅在未通过绑定调用命令的情况下公开ExecuteAsync提供任务。

这个命令不是所有异步问题的答案,但对于我遇到的大多数情况来说它是一个简单的命令。

使用异步命令永久链接

一切就绪后,我们可以修改视图模型以使用AsyncCommand

public class MainViewModel : ViewModelBase
{
    private bool _isBusy;
    public bool IsBusy
    {
        get => _isBusy;
        private set => Set(ref _isBusy, value);
    }

    public IAsyncCommand Submit { get; private set; }

    public MainViewModel()
    {
        Submit = new AsyncCommand(ExecuteSubmitAsync, CanExecuteSubmit);
    }

    private async Task ExecuteSubmitAsync()
    {
        try
        {
            IsBusy = true;
            var coffeeService = new CoffeeService();
            await coffeeService.PrepareCoffeeAsync();
        }
        finally
        {
            IsBusy = false;
        }
    }

    private bool CanExecuteSubmit()
    {
        return !IsBusy;
    }
}

如您所见,视图模型中没有 async void !

调用命令永久链接

该命令很容易通过以下方式调用:

  • 绑定
  • 代码
    public void OnPrepareButtonClick(object sender, EventArgs e)
    {
      IErrorHandler errorHandler = null; // Get an instance somewhere
      ViewModel.Submit.ExecuteAsync().FireAndForgetSafeAsync(errorHandler);
    }

通用版永久链接

对于所有需要参数的命令,我们可以使用以下通用版本:

public interface IAsyncCommand : ICommand
{
    Task ExecuteAsync(T parameter);
    bool CanExecute(T parameter);
}

public class AsyncCommand : IAsyncCommand
{
    public event EventHandler CanExecuteChanged;

    private bool _isExecuting;
    private readonly Func _execute;
    private readonly Func _canExecute;
    private readonly IErrorHandler _errorHandler;

    public AsyncCommand(Func execute, Func canExecute = null, IErrorHandler errorHandler = null)
    {
        _execute = execute;
        _canExecute = canExecute;
        _errorHandler = errorHandler;
    }

    public bool CanExecute(T parameter)
    {
        return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
    }

    public async Task ExecuteAsync(T parameter)
    {
        if (CanExecute(parameter))
        {
            try
            {
                _isExecuting = true;
                await _execute(parameter);
            }
            finally
            {
                _isExecuting = false;
            }
        }

        RaiseCanExecuteChanged();
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

#region Explicit implementations
    bool ICommand.CanExecute(object parameter)
    {
        return CanExecute((T)parameter);
    }

    void ICommand.Execute(object parameter)
    {
        ExecuteAsync((T)parameter).FireAndForgetSafeAsync(_errorHandler);
    }
#endregion
}

这两个版本非常相似,很容易只保留后者。我们可以使用一个AsyncCommand带空参数来替换第一个。虽然它在技术上有效,但最好将它们两个保留在没有参数与采用空参数在语义上不相似的意义上。

结论永久链接

通过编写一个本机处理异步的简单自定义命令,我们能够简化和改进我们的代码和应用程​​序的稳定性。将方法嵌入FireAndForgetSafeAsync命令中消除了我们忘记处理异常的可能性。

请不要忘记崩溃是不可接受的,完全是我们的错

这篇文章的源代码可以在我的GitHub 上找到。

一如既往,请随时阅读我以前的帖子并在下面发表评论,我将非常乐意回答。

你可能感兴趣的:(MVVM,WPF,wpf,c#)