MVVM异步等待模式

wax*_*cal 9 c# wpf asynchronous mvvm async-await

我一直在尝试为WPF应用程序编写MVVM屏幕,使用async和await关键字为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;
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

IsLoading暴露在绑定到忙指示符的视图中.

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

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

Ste*_*ary 19

跳出来的代码中有一些问题:

  • 用法ContinueWith.ContinueWith是一个危险的API(它有一个令人惊讶的默认值TaskScheduler,因此它应该只在你指定a时使用TaskScheduler).与等效await代码相比,它也只是简单的尴尬.
  • Scenarios从线程池线程设置.我始终遵循我的代码中的指南,即数据绑定的VM属性被视为UI的一部分,并且只能从UI线程访问.这条规则有例外(特别是在WPF上),但它们在每个MVVM平台上都不一样(并且一开始就是一个值得怀疑的设计,IMO),因此我只将虚拟机视为UI层的一部分.
  • 抛出异常的地方.根据评论,您希望提出异常Application.UnhandledException,但我不认为此代码会这样做.假设TaskScheduler.CurrentnullLoadDataAsync/ 的开头SaveAsync,那么重新提升的异常代码实际上会在线程池线程而不是UI线程上引发异常,从而将其发送到AppDomain.UnhandledException而不是Application.UnhandledException.
  • 如何重新抛出异常.你将失去堆栈跟踪.
  • LoadDataAsync没有电话await.使用这个简化的代码,它可能会工作,但它确实引入了忽略未处理的异常的可能性.特别是,如果LoadDataAsync抛出任何同步部分,那么该异常将被默默地忽略.

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

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

(async void我所指的方法是async传递给的代表DelegateCommand).

代码现在变成:

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();
  }
}
Run Code Online (Sandbox Code Playgroud)

现在所有问题都已解决:

  • ContinueWith已被替换为更合适await.
  • Scenarios 是从UI线程设置的.
  • 所有异常都传播到Application.UnhandledException而不是AppDomain.UnhandledException.
  • 异常保持其原始堆栈跟踪.
  • 没有任何未await执行的任务,因此将以某种方式观察所有异常.

而且代码也更清晰.IMO.:)