MVVM异步等待模式

时间:2016-08-02 10:09:11

标签: c# wpf asynchronous mvvm async-await

我一直在尝试使用async& amp;来为WPF应用程序编写MVVM屏幕。等待关键字为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方法,然后触发刷新。

1 个答案:

答案 0 :(得分:17)

代码中有一些问题突然出现在我面前:

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

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

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

(我引用的async void方法是传递给async的{​​{1}}个代表。

现在代码变为:

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(); } } 已替换为更合适的ContinueWith
  • await是从UI线程设置的。
  • 所有异常都会传播到Scenarios而不是Application.UnhandledException
  • 例外保持原始堆栈跟踪。
  • 没有非AppDomain.UnhandledException - ed任务,所以所有异常都会以某种方式被观察到。

代码也更清晰。 IMO。 :)