我在我的ViewModels中使用DelegateCommands(Prism),我将其作为ICommands公开给外部。
caviat是:DelegateCommand.Execute实现为Task Execute(...)而ICommand.Execute实现为简单的void Execute(...)。
我注意到了这一点,因为在执行处理程序中吞下了异常。虽然这是asyncs的一种典型行为,但我并不期待ICommand.Execute(它没有异步的迹象)。
如果我执行ICommand,我将无法捕获DelegateCommand最终抛出的异常,因为DelegateCommands Execute()方法是异步的,而ICommands则不是。
使用DelegateCommand作为ICommand时,有没有办法捕获抛出的异常?
[Test]
public void DelegateToICommandExecute()
{
var dCommand = new DelegateCommand(() => { throw new Exception(); });
ICommand command = dCommand;
command.Execute(null); // Doesn't fail due to exception
}
将nUnit测试用例作为异步工作,但是visual studio抱怨我有一个异步方法而不等待await ICommand.Execute
是不可能的。
将它显式地转换为DelegateCommand是可能的,但这只会修复单元测试,而不是修复抛出异常时应用程序的行为。
使用ICommand
的应用程序如何处理吞噬异常的异步底层调用?
DelegateBase(DelegateCommand从中继承)将其Execute定义为async void Execute
,然后等待其自己的Task Execute()
调用。
因此,在调用ICommand.Execute时,我最终会在引擎盖下有效地调用异步空白。
答案 0 :(得分:2)
在执行处理程序中吞下了异常。
他们当然不应该。根据{{3}},ICommand.Execute
(正确)实现为async void
方法,await
是异步命令。
这意味着ICommand.Execute
来电是而不是吞噬异常。但是,它也无法直接捕获,因为是异步方法。我将在the source code中详细说明具体情况:在这种情况下,异常会在原始调用ICommand.Execute
的上下文中重新提出。
当从UI线程调用ICommand.Execute
时(即通过MVVM绑定),然后在UI线程上引发该异常,并且该UI框架的任何默认行为都从那里获取(通常有一个最后机会处理程序后跟对话框/模态)。但是当它从单元测试调用时,它使用单元测试框架提供的任何上下文。我进一步描述了异步单元测试Async Best Practices article,但其要点是:如果你进行单元测试async void
,那么(当前版本)NUnit会给你一个上下文。但不要依赖这种行为;它已经被认为是一个糟糕的设计决定,将从下一版本的NUnit v3中删除。如果单元测试框架不提供上下文(应该是这种情况,并且将来将是这种情况),那么将在线程池上下文中重新引发异常,这将导致测试运行器中的任意线程失败。测试运行器如何响应这一点是不确定的:事实上,如果你只有一个测试,测试运行器可能会在看到异常之前完成,所以它确实看起来“丢失”了。测试运行器也可能忽略与特定测试无法匹配的异常。
相反,解决方案是双重的:
DelegateCommand
类型,而不是ICommand
。这是不幸的,我希望Prism有一个IAsyncCommand
,你可以反映它,但它就是它。 (FWIW,我总是使用自己的AsyncCommand
来实现IAsyncCommand
)。async Task
(不是async void
),然后自然地执行await
命令的执行。Execute
(而不是使用命令绑定),那么它也应该更新为async Task
(或async Task<T>
)和{{1从await
返回的任务。请注意Execute
中的异常在运行时忽略 ,但它与从事件处理程序引发的异常具有相同的效果:必须全局处理它它应该被处理。这通常不是你想要的。这对于异步命令尤其是一个问题,因为它们通常涉及容易出错的I / O操作,而这些操作要优雅地处理。
要解决这个“元问题”,您需要重新审视您希望异步命令的行为。将ICommand.Execute
/ try
放在委托的顶部并更新数据绑定属性(如果它失败)并不罕见。我在我的in another MSDN article中探索了各种类似的解决方案,但这种情况下“一刀切”当然不适用。