当用作ICommand时,由ASYNC DelegateCommand.Execute()引发的捕获异常

本文关键字:Execute 捕获异常 DelegateCommand ASYNC ICommand | 更新日期: 2023-09-27 18:32:08

我在ViewModels中使用DelegateCommand(Prism),我将其作为ICommands公开给外部。

caviat是:DelegateCommand.Execute被实现为Task Execute(...),而ICommand.Execute被实现为简单的void Execute(...)。

我注意到了这一点,因为在执行处理程序中吞噬了异常。虽然这是不等待的异步的典型行为,但我没想到 ICommand.Execute(它没有异步的迹象)会发生这种情况。

如果我执行 ICommand,我将无法捕获 DelegateCommand 最终抛出的异常,因为 DelegateCommands Execute() 方法是异步的,而 ICommands 不是。

当使用委托命令作为 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 时,我最终有效地调用了引擎盖下的异步空隙。

当用作ICommand时,由ASYNC DelegateCommand.Execute()引发的捕获异常

异常在执行处理程序中被吞噬。

他们当然不应该是。根据源代码,ICommand.Execute(正确)实现为await异步命令的async void方法。

这意味着ICommand.Execute调用不会吞噬异常。但是,也不能直接捕获它,因为它是一种异步方法。我在异步最佳实践文章中详细描述了发生的情况:在这种情况下,异常将在原始调用ICommand.Execute的上下文中重新引发。

当从 UI 线程

(即通过 MVVM 绑定)调用ICommand.Execute时,该异常会在 UI 线程上引发,并且该 UI 框架的任何默认行为都会从那里获取它(通常有一个最后机会处理程序,后跟一个对话框/模态)。但是,当从单元测试调用它时,它使用单元测试框架提供的任何上下文。我在另一篇 MSDN 文章中进一步描述了异步单元测试,但它的要点是:如果你将单元测试设为async void,那么(当前版本的)NUnit 将为您提供上下文。但不要依赖这种行为;它已被公认为一个糟糕的设计决策,将从下一版本的 NUnit v3 中删除。如果单元测试框架没有提供上下文(应该是这种情况,将来也会如此),那么将在线程池上下文中重新引发异常,这将导致测试运行器中的任意线程失败。测试运行程序对此的反应是不确定的:事实上,如果您只有一个测试,则测试运行程序可能会在看到异常之前完成,因此它确实看起来"丢失"了。测试运行程序也可能忽略无法与特定测试匹配的异常。

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

  1. 将视图模型属性公开为类型 DelegateCommand 而不是 ICommand 。这是不幸的,我希望棱镜有一个你可以曝光的IAsyncCommand,但它就是这样。(FWIW,我总是使用我自己的AsyncCommand来实现IAsyncCommand)。
  2. 使单元测试async Task(而不是async void),然后自然地await命令的执行。
  3. 如果任何代码直接调用Execute(而不是使用命令绑定),则还应将其更新为async Task(或async Task<T>)并awaitExecute返回的任务。

请注意,ICommand.Execute 中的异常在运行时不会被忽略,但它与从事件处理程序引发的异常具有相同的效果:如果要处理它,则必须全局处理它。这通常不是您想要的。这对于异步命令来说尤其是一个问题,因为它们通常涉及容易出现您希望正常处理的错误 I/O 操作。

要解决这个"元问题",您需要重新审视您希望异步命令的确切行为方式。仅在委托的顶部放置一个 try/catch,并在失败时更新数据绑定属性的情况并不少见。我在有关异步 MVVM 命令的 MSDN 文章中探讨了各种类似的解决方案,但在这种情况下,"一刀切"当然不适用。