当用作ICommand时,由ASYNC DelegateCommand.Execute()抛出的Catch Exception

时间:2014-11-27 09:52:17

标签: c# asynchronous nunit prism

我在我的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时,我最终会在引擎盖下有效地调用异步空白。

1 个答案:

答案 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中删除。如果单元测试框架提供上下文(应该是这种情况,并且将来将是这种情况),那么将在线程池上下文中重新引发异常,这将导致测试运行器中的任意线程失败。测试运行器如何响应这一点是不确定的:事实上,如果你只有一个测试,测试运行器可能会在看到异常之前完成,所以它确实看起来“丢失”了。测试运行器也可能忽略与特定测试无法匹配的异常。

相反,解决方案是双重的:

  1. 将ViewModel属性公开为DelegateCommand类型,而不是ICommand。这是不幸的,我希望Prism有一个IAsyncCommand,你可以反映它,但它就是它。 (FWIW,我总是使用自己的AsyncCommand来实现IAsyncCommand)。
  2. 进行单元测试async Task(不是async void),然后自然地执行await命令的执行。
  3. 如果您的任何代码直接调用Execute(而不是使用命令绑定),那么它也应该更新为async Task(或async Task<T>)和{{1从await返回的任务。
  4. 请注意Execute中的异常在运行时忽略 ,但它与从事件处理程序引发的异常具有相同的效果:必须全局处理它它应该被处理。这通常不是你想要的。这对于异步命令尤其是一个问题,因为它们通常涉及容易出错的I / O操作,而这些操作要优雅地处理

    要解决这个“元问题”,您需要重新审视您希望异步命令的行为。将ICommand.Execute / try放在委托的顶部并更新数据绑定属性(如果它失败)并不罕见。我在我的in another MSDN article中探索了各种类似的解决方案,但这种情况下“一刀切”当然不适用。