我一直在尝试使用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方法,然后触发刷新。
答案 0 :(得分:17)
代码中有一些问题突然出现在我面前:
ContinueWith
的用法。 ContinueWith
是一个危险的API(它的TaskScheduler
有一个令人惊讶的默认值,因此只有在指定TaskScheduler
时才会使用它。与等效的await
代码相比,它也显得很尴尬。Scenarios
。我始终遵循我的代码中的指南,即数据绑定的VM属性被视为UI的一部分,并且只能从UI线程访问。这个规则有一些例外(特别是在WPF上),但它们在每个MVVM平台上都不一样(并且一开始就是一个值得怀疑的设计,IMO),所以我只是将虚拟机视为UI层的一部分。 Application.UnhandledException
,但我不认为此代码会这样做。假设在TaskScheduler.Current
/ null
的开头LoadDataAsync
为SaveAsync
,则重新提升的异常代码实际上会在线程池上引发异常线程,而不是 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。 :)