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方法,然后触发刷新。
我突然想到代码中有几个问题:
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();
}
}
现在所有的问题都解决了:
- CCD_ 26已经被更合适的CCD_
- CCD_ 28是从UI线程设置的
- 所有异常都传播到
Application.UnhandledException
而不是AppDomain.UnhandledException
- 异常保留其原始堆栈跟踪
- 不存在未经
await
处理的任务,因此会以某种方式观察到所有异常
代码也更干净。IMO.:)