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.Current
在LoadDataAsync
/SaveAsync
的开头是null
,那么重新引发异常代码实际上会在线程池线程上引发异常,而不是 UI 线程,因此将其发送到 AppDomain.UnhandledException
而不是 Application.UnhandledException
。
如何重新抛出异常。您将丢失堆栈跟踪。
在没有await
的情况下调用LoadDataAsync
。使用这个简化的代码,它可能会工作,但它确实引入了忽略未处理异常的可能性。特别是,如果 LoadDataAsync
的任何同步部分抛出,那么该异常将被静默忽略。
与其搞乱手动异常重新抛出,我建议只使用通过await
传播异常的更自然的方法:
await
将检查此异常,并以适当的方式重新引发它(保留原始堆栈跟踪)。
async void
方法没有放置异常的任务,因此它们将直接在 SynchronizationContext
上重新引发异常。在这种情况下,由于您的 async void
方法在 UI 线程上运行,异常将被发送到 Application.UnhandledException
。
(我所指的async void
方法是传递给DelegateCommand
的async
代表)。
代码现在变成:
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
。
Scenarios
是从 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
之外,否则应该可以正常工作。注意如果LoadData
是async void
,那么这会导致问题——如果实现需要async
,那么抽象方法应该返回Task
。以上是关于MVVM 异步等待模式的主要内容,如果未能解决你的问题,请参考以下文章
通过节点的异步/等待不需要按预期顺序返回结果 - 使用啥正确模式?
.NET Core: 用 Fire & forget 模式执行无需等待的异步操作