在“async void” WPF命令处理程序中的异常处理

12

我正在审查我的同事们的一些WPF代码,这是一个基于UserControl 的组件库,其中有许多async void事件和命令处理程序。这些方法目前在内部没有实现任何错误处理

简而言之,代码如下:

<Window.CommandBindings>
    <CommandBinding
        Command="ApplicationCommands.New"
        Executed="NewCommand_Executed"/>
</Window.CommandBindings>
private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    // do some fake async work (and may throw if timeout < -1)
    var timeout = new Random(Environment.TickCount).Next(-100, 100);
    await Task.Delay(timeout);
}

NewCommand_Executed内部抛出但未被观察的异常只能在全局级别(例如,使用AppDomain.CurrentDomain.UnhandledException)处理。 显然,这不是一个好主意。

我可以在本地处理异常:

private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    try
    {
        // do some fake async work (throws if timeout < -1)
        var timeout = new Random(Environment.TickCount).Next(-100, 100);
        await Task.Delay(timeout);
    }
    catch (Exception ex)
    {
        // somehow log and report the error
        MessageBox.Show(ex.Message);
    }
}

然而,在这种情况下,主应用程序的ViewModel无法感知NewCommand_Executed中的错误。这也不是一个理想的解决方案,而且错误报告UI不应该总是成为库代码的一部分。

另一种方法是在本地处理错误并触发专门的错误事件:

public class AsyncErrorEventArgs: EventArgs
{
    public object Sender { get; internal set; }
    public ExecutedRoutedEventArgs Args { get; internal set; }
    public ExceptionDispatchInfo ExceptionInfo { get; internal set; }
}

public delegate void AsyncErrorEventHandler(object sender, AsyncErrorEventArgs e);

public event AsyncErrorEventHandler AsyncErrorEvent;

private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    ExceptionDispatchInfo exceptionInfo = null;

    try
    {
        // do some fake async work (throws if timeout < -1)
        var timeout = new Random(Environment.TickCount).Next(-100, 100);
        await Task.Delay(timeout);
    }
    catch (Exception ex)
    {
        // capture the error
        exceptionInfo = ExceptionDispatchInfo.Capture(ex);
    }

    if (exceptionInfo != null && this.AsyncErrorEvent != null)
        this.AsyncErrorEvent(sender, new AsyncErrorEventArgs { 
            Sender = this, Args = e, ExceptionInfo = exceptionInfo });
}

我最喜欢最后一个,但由于我对WPF的经验有限,我会感激任何其他建议。

  • 在WPF中是否有一种既定的模式来将 async void 命令处理程序中的错误传播到ViewModal中?

  • 通常在WPF命令处理程序内部进行异步工作是否是个坏主意,因为它们可能是用于快速同步UI更新的?

我在WPF的背景下提出了这个问题,但我认为它同样适用于WinForms中的 async void 事件处理程序。


1
这个MSDN线程可能会为您的问题提供一些启示。 - Ilya Tereschuk
@ElliotTereschuk,谢谢你,那个线程引用了另一篇好文章。然而,据我所知,那里的AwaitableDelegateCommand.Execute仍然不会传播任何内部异常。因此,我不确定我们是否可以将其与XAML声明式命令绑定一起使用。 - noseratio - open to work
为什么要多余的变量?就像上面所描述的那样。我想在catch之外执行它。 - noseratio - open to work
为什么你将ExceptionDispatchInfo发送到通知而不是异常本身呢?这样我就可以在另一个堆栈帧上使用ExceptionDispatchInfo.Throw()来重新抛出它,并具有相同的调试信息,更多信息请见:https://dev59.com/P2Qn5IYBdhLWcg3wtpBW。 - noseratio - open to work
1
@PauloMorgado,这是一个“UserControl”库,它对于ViewModal或任何外部客户端代码一无所知。通过像这样触发错误事件,我给了这样的代码机会以任何期望的方式处理/报告/记录错误。客户端代码的开发人员可以选择通过AsyncErrorEventArgs.ExceptionInfo.SourceException检查异常或通过AsyncErrorEventArgs.ExceptionInfo.Throw()重新抛出异常或两者都执行。如果您有其他解决方案,请随时发布答案,这就是问题所在。 - noseratio - open to work
显示剩余6条评论
2个回答

8
问题在于您的UserControl库没有按照典型的MVVM方式进行架构。通常情况下,对于非平凡命令,您的UserControl代码不会直接绑定到命令,而是会拥有一些属性,当这些属性被设置(通过与ViewModel的绑定)时,会触发控件中的操作。然后,您的ViewModel将绑定到应用程序命令,并设置适当的属性。(或者,您的MVVM框架可能有另一种消息传递场景,可以利用ViewModel和View之间的交互)。
至于UI内部抛出的异常,我再次感到存在架构问题。如果UserControl不仅仅充当视图(即运行任何可能导致意外异常的业务逻辑),则应将其分成视图和ViewModel。ViewModel将运行逻辑,可以由其他应用程序ViewModel实例化,或通过另一种方法进行通信(如上所述)。
如果UserControl的布局/可视化代码引发异常,那么这几乎不可能在任何情况下被ViewModel捕获。正如您所提到的,这应该仅限于全局级别处理程序记录日志。
最后,如果确实在控件代码中存在已知的“异常”,需要通知您的ViewModel,则建议捕获已知的异常并引发事件/命令并设置属性。但同样,这真的不应用于异常,只用于预期的“错误”状态。

1
另一层分离确实有意义,+1。然而,对于ViewModel来说,问题仍然存在。例如,假设NewCommand_Executed是ViewModal的一部分,并且控件公开LoadAsync API,在NewCommand_Executed内部调用。 - noseratio - open to work
1
我想我现在明白了。然后,ViewModal可以在async void处理程序内部进行简单的try/catch,并以其认为合适的方式报告错误。我会让这个问题悬而未决一段时间,但我认为赏金是你的,而且非常值得,谢谢! - noseratio - open to work
2
据我所知,UserControls“应该”包括针对RoutedUICommands(例如ApplicationCommands.Paste)的CommandBinding代码,与您在第一段中所说的相反。 您能澄清一下吗? - JoeGaggler
@AndrewHanlon,您能否指出一些关于在UserControl中公开依赖属性与绑定应用程序命令的可靠MVVM资源? - noseratio - open to work
2
@JoeGaggler 您的确是正确的,纯粹影响UI的应用程序命令(如Paste)可以直接绑定和处理在用户控件中。我的观点是任何使用异步或可能引发异常(需要处理)的命令都不应该这样做。 - Andrew Hanlon
2
未来参考的相关链接:Commands、RelayCommands 和 EventToCommand - noseratio - open to work

4
我认为,在用户几乎完全不知情的情况下传播异常并不是一个好的做法。请参见此处
我认为你有两个选择,因为WPF没有提供任何这样的机制来通知任何问题:
  1. 像你已经提供的那样捕获并触发事件。
  2. 从异步方法返回Task对象(在你的情况下,似乎你将不得不通过属性公开它)。用户将能够检查执行过程中是否有任何错误,并在需要时附加继续任务。在处理程序内部,您可以捕获任何异常并使用TaskCompletionSource设置处理程序的结果。
总之,你必须为这样的代码编写一些xml注释,因为这并不容易理解。 最重要的是,你几乎不应该从任何辅助线程抛出任何异常。

我确实想让用户意识到错误,但是我不想将错误消息 UI 封装在控件内部。使用 Task 作为控件属性与事件方法的问题在于,由于异步命令可能会重叠,它可能必须是一个 List<Task> 属性。我正在考虑第三个选项:将客户端 UI 错误报告对象注入控件中(可以通过构造函数传递或设置为属性)。尽管这与事件方法的方法并没有太大区别。 - noseratio - open to work
是的,你说得对。那么你想知道是否有任何特定于WPF的方式来解决这个问题。不幸的是,答案是否定的。 - EngineerSpock

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接