MVVM异步等待模式

本文关键字:模式 等待 异步 MVVM | 更新日期: 2023-09-27 17:59:57

我一直在尝试为WPF应用程序编写MVVM屏幕,使用async&等待关键字为1编写异步方法。最初加载数据,2。正在刷新数据,3。保存更改,然后刷新。虽然我已经完成了这项工作,但代码非常混乱,我忍不住想一定有更好的实现。有人能就更简单的实现提出建议吗?

这是我的ViewModel:的精简版

public class ScenariosViewModel : BindableBase
{
    public ScenariosViewModel()
    {
        SaveCommand = new DelegateCommand(async () => await SaveAsync());
        RefreshCommand = new DelegateCommand(async () => await LoadDataAsync());
    }
    public async Task LoadDataAsync()
    {
        IsLoading = true; //synchronously set the busy indicator flag
        await Task.Run(() => Scenarios = _service.AllScenarios())
            .ContinueWith(t =>
            {
                IsLoading = false;
                if (t.Exception != null)
                {
                    throw t.Exception; //Allow exception to be caught on Application_UnhandledException
                }
            });
    }
    public ICommand SaveCommand { get; set; }
    private async Task SaveAsync()
    {
        IsLoading = true; //synchronously set the busy indicator flag
        await Task.Run(() =>
        {
            _service.Save(_selectedScenario);
            LoadDataAsync(); // here we get compiler warnings because not called with await
        }).ContinueWith(t =>
        {
            if (t.Exception != null)
            {
                throw t.Exception;
            }
        });
    }
}

IsLoading在绑定到繁忙指示器的视图中公开。

当首次查看屏幕或按下刷新按钮时,导航框架会调用LoadDataAsync。此方法应同步设置IsLoading,然后将控制权返回到UI线程,直到服务返回数据为止。最后抛出任何异常,以便全局异常处理程序能够捕获它们(不进行讨论!)。

SaveAync由一个按钮调用,将更新后的值从表单传递给服务。它应该同步设置IsLoading,异步调用服务上的Save方法,然后触发刷新。

MVVM异步等待模式

我突然想到代码中有几个问题:

  • ContinueWith的用法。ContinueWith是一个危险的API(它的TaskScheduler有一个令人惊讶的默认值,所以只有在指定TaskScheduler时才应该使用它)。与等效的await代码相比,它也很尴尬
  • 从线程池线程设置Scenarios。我一直遵循代码中的指导原则,即数据绑定的VM属性被视为UI的一部分,并且只能从UI线程访问。这个规则也有例外(特别是在WPF上),但它们在每个MVVM平台上都不一样(从一开始就是一个有问题的设计,IMO),所以我只是将VM视为UI层的一部分
  • 抛出异常的位置。根据评论,您希望将异常提升到Application.UnhandledException,但我认为这段代码不会做到这一点。假设TaskScheduler.CurrentLoadDataAsync/SaveAsync开始时是null,那么重新引发异常代码实际上会在线程池线程上引发异常,而不是在UI的线程上,从而将其发送到AppDomain.UnhandledException而不是Application.UnhandledException
  • 如何重新抛出异常。您将丢失堆栈跟踪
  • 在没有await的情况下调用LoadDataAsync。有了这个简化的代码,它可能会起作用,但它确实引入了忽略未处理异常的可能性。特别是,如果LoadDataAsync的任何同步部分抛出,那么该异常将被静默地忽略

我不建议手动重新抛出异常,而是建议使用通过await:传播异常的更自然的方法

  • 如果异步操作失败,则任务将获得一个异常
  • await将检查此异常,并以适当的方式重新引发它(保留原始堆栈跟踪)
  • async void方法没有用于放置异常的任务,因此它们将直接在其SynchronizationContext上重新引发异常。在这种情况下,由于async void方法在UI线程上运行,因此异常将发送到Application.UnhandledException

(我所指的async void方法是传递给DelegateCommandasync委托)。

代码现在变为:

public class ScenariosViewModel : BindableBase
{
  public ScenariosViewModel()
  {
    SaveCommand = new DelegateCommand(async () => await SaveAsync());
    RefreshCommand = new DelegateCommand(async () => await LoadDataAsync());
  }
  public async Task LoadDataAsync()
  {
    IsLoading = true;
    try
    {
      Scenarios = await Task.Run(() => _service.AllScenarios());
    }
    finally
    {
      IsLoading = false;
    }
  }
  private async Task SaveAsync()
  {
    IsLoading = true;
    await Task.Run(() => _service.Save(_selectedScenario));
    await LoadDataAsync();
  }
}

现在所有的问题都解决了:

  • CCD_ 26已经被更合适的CCD_
  • CCD_ 28是从UI线程设置的
  • 所有异常都传播到Application.UnhandledException而不是AppDomain.UnhandledException
  • 异常保留其原始堆栈跟踪
  • 不存在未经await处理的任务,因此会以某种方式观察到所有异常

代码也更干净。IMO.:)