MVVM 异步等待模式

Posted

技术标签:

【中文标题】MVVM 异步等待模式【英文标题】:MVVM async await pattern 【发布时间】:2016-12-07 15:17:06 【问题描述】:

我一直在尝试为 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;
            
        );
    

IsLoading 暴露给绑定到繁忙指示器的视图。

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

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

【问题讨论】:

你检查了吗? msdn.microsoft.com/en-us/magazine/dn605875.aspx. 是的,这是一篇很棒的文章。我不确定我是否喜欢绑定到 Something.Result,但感觉 ViewModel 应该让它的状态比这更明显。 只是一个尝试的想法...创建一个标准的 getter only 属性,并在 get 中等待Something。使用 IsAsync=true 进行绑定。 如果您需要帮助改进工作代码,Code Review 【参考方案1】:

我跳出来的代码中有几个问题:

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

现在所有问题都解决了:

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

而且代码也更简洁。国际海事组织。 :)

【讨论】:

嗨,斯蒂芬,感谢您提供如此完整的答案。这是对我的代码的巨大改进。 LoadDataAsync 方法实际上位于我用于 ViewModel 的基类上,它调用抽象方法 loadData,该方法调用特定服务并设置特定属性。有什么办法可以保留它并仍然在 UI 线程上设置属性?受保护的抽象 void loadData();受保护的虚拟异步任务 loadDataAsync() IsLoading = true;等待 Task.Run(() => loadData(); IsLoading = false; ); @waxingsatirical:您希望将IsLoading = false 移到Task.Run 之外,否则应该可以正常工作。注意如果LoadDataasync void,那么这会导致问题——如果实现需要async,那么抽象方法应该返回Task

以上是关于MVVM 异步等待模式的主要内容,如果未能解决你的问题,请参考以下文章

传递异步方法真的需要等待/异步模式吗? [复制]

通过节点的异步/等待不需要按预期顺序返回结果 - 使用啥正确模式?

设计模式(等待者模式)

.NET Core: 用 Fire & forget 模式执行无需等待的异步操作

.NET Core: 用 Fire & forget 模式执行无需等待的异步操作

.NET Core: 用 Fire & forget 模式执行无需等待的异步操作