当用作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.Execute
(正确)实现为await
异步命令的async void
方法。
这意味着ICommand.Execute
调用不会吞噬异常。但是,也不能直接捕获它,因为它是一种异步方法。我在异步最佳实践文章中详细描述了发生的情况:在这种情况下,异常将在原始调用ICommand.Execute
的上下文中重新引发。
(即通过 MVVM 绑定)调用ICommand.Execute
时,该异常会在 UI 线程上引发,并且该 UI 框架的任何默认行为都会从那里获取它(通常有一个最后机会处理程序,后跟一个对话框/模态)。但是,当从单元测试调用它时,它使用单元测试框架提供的任何上下文。我在另一篇 MSDN 文章中进一步描述了异步单元测试,但它的要点是:如果你将单元测试设为async void
,那么(当前版本的)NUnit 将为您提供上下文。但不要依赖这种行为;它已被公认为一个糟糕的设计决策,将从下一版本的 NUnit v3 中删除。如果单元测试框架没有提供上下文(应该是这种情况,将来也会如此),那么将在线程池上下文中重新引发异常,这将导致测试运行器中的任意线程失败。测试运行程序对此的反应是不确定的:事实上,如果您只有一个测试,则测试运行程序可能会在看到异常之前完成,因此它确实看起来"丢失"了。测试运行程序也可能忽略无法与特定测试匹配的异常。
相反,解决方案是双重的:
- 将视图模型属性公开为类型
DelegateCommand
而不是ICommand
。这是不幸的,我希望棱镜有一个你可以曝光的IAsyncCommand
,但它就是这样。(FWIW,我总是使用我自己的AsyncCommand
来实现IAsyncCommand
)。 - 使单元测试
async Task
(而不是async void
),然后自然地await
命令的执行。 - 如果任何代码直接调用
Execute
(而不是使用命令绑定),则还应将其更新为async Task
(或async Task<T>
)并await
从Execute
返回的任务。
请注意,ICommand.Execute
中的异常在运行时不会被忽略,但它与从事件处理程序引发的异常具有相同的效果:如果要处理它,则必须全局处理它。这通常不是您想要的。这对于异步命令来说尤其是一个问题,因为它们通常涉及容易出现您希望正常处理的错误 I/O 操作。
要解决这个"元问题",您需要重新审视您希望异步命令的确切行为方式。仅在委托的顶部放置一个 try
/catch
,并在失败时更新数据绑定属性的情况并不少见。我在有关异步 MVVM 命令的 MSDN 文章中探讨了各种类似的解决方案,但在这种情况下,"一刀切"当然不适用。