WPF UnhandledException

WPF UnhandledException

在 WPF 程序中,通常可以通过 Application.DispatcherUnhandledExceptionAppDomain.UnhandledException 事件来处理全局 未处理异常,其中前者是由 WPF 框架提供的,后者是由 .NET Framework 提供的,后者能够捕获更多的未处理异常。对于 Task 中的未处理异常,这两种事件都不会触发,仅能通过 TaskScheduler.UnobservedTaskException 事件来捕获。另外,还有个 AppDomain.FirstChanceException 事件,每个异常都会引发该事件,即使该异常已被 try...catch 处理,此事件不在本文的讨论范围内。

  • Application.DispatcherUnhandledException 事件
    • 能够捕获 UI 线程抛出的未处理异常
    • 可通过事件参数 e.Handled = true 来阻止程序崩溃
  • AppDomain.UnhandledException 事件
    • 能捕获 所有线程(Task 除外) 抛出的未处理异常
    • 默认情况无法阻止程序崩溃(可通过 legacyUnhandledExceptionPolicy 配置异常策略 )
  • TaskScheduler.UnobservedTaskException
    • 仅能捕获 Task 中抛出的未处理异常
    • 事件的触发有延时,依赖垃圾回收

注册异常事件

protected override void OnStartup(StartupEventArgs e)
{
    AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    Application.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException;

    base.OnStartup(e);
}

private void Current_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
    MessageBox.Show($"Current_DispatcherUnhandledException:{e.Exception}");
    e.Handled = true;  // 标记为 “已处理”,避免异常进一步传递而引起崩溃
}

private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    MessageBox.Show($"CurrentDomain_UnhandledException: {e.ExceptionObject}");
}

UI 线程异常

private void Button_Click(object sender, RoutedEventArgs e)
{
    throw new InvalidOperationException();
}

对于一个 UI 线程抛出的未处理异常,其会先触发 DispatcherUnhandledException 事件,如果该事件处理方法中未标记 e.Handledtrue,则会进一步触发 UnhandledException 事件。

Thread 异常

private void Button_Click(object sender, RoutedEventArgs e)
{
    Thread thread = new Thread(() => { throw new InvalidOperationException(); });
    thread.Start();

    // ThreadPool.QueueUserWorkItem(state => { throw new InvalidOperationException(); });
}

此类未捕获异常仅会触发 UnhandledException 事件,并且事件参数中并未提供类似 e.Handled 的方法来阻止程序崩溃,通常仅在该事件处理方法中添加日志记录或用户提示。在 .NET 2.0 及以前的版本,此类未处理异常是不会引起程序崩溃的,我们也可以通过配置来开启旧的异常处理策略,在 App.Config 中添加如下配置:

<configuration>
  <runtime>
    <legacyUnhandledExceptionPolicy enabled="1"/>
  runtime>
configuration>

Task 异常

private void Button_Click(object sender, RoutedEventArgs e)
{
    var task = new Task(() =>
    {
        throw new InvalidOperationException();
    });
    task.Start();

    // Task.Run(() => throw new InvalidOperationException());
    // Task.Factory.StartNew(() => throw new InvalidOperationException());

    // var worker = new BackgroundWorker();
    // worker.DoWork += (s, ex) => { throw new InvalidOperationException(); };
    // worker.RunWorkerAsync();
}

如上的代码不会触发 UnhandledException 事件,也不会引起程序奔溃。如果想从全局捕获此类未处理异常,可注册 TaskScheduler.UnobservedTaskException 事件。

protected override void OnStartup(StartupEventArgs e)
{
    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
    base.OnStartup(e);
}

private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
    MessageBox.Show($"TaskScheduler_UnobservedTaskException: {e.Exception}");
    e.SetObserved();  // 标识异常已被观察,不会传给系统,避免崩溃
}

此事件并非在抛出异常后立即触发,其依赖于垃圾回收,在某次垃圾收集过程,从 Finalizer 线程里触发并执行。可通过如下方式来强制垃圾回收,及时触发事件(实际工程中避免这些操作,会有性能问题)。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var task = Task.Run(() => throw new NotImplementedException());

    ((IAsyncResult)task).AsyncWaitHandle.WaitOne();
    task = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}

另外,还可将 Task 中的异常转到调度线程中,从而引发 UnhandledException 事件,Task.Result、Task.Wait() 等都可实现此效果。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var task = Task.Run(() => throw new NotImplementedException());
    task.Wait();
}

顽固的异常

托管代码 中调用 非托管 接口,部分未处理异常是无法接住的,会直接引起程序崩溃。如下所示,C++ 中实现了 Add(int x, int y) 方法,在 C# 中调用之,前面的未处理异常事件均不会触发,程序会直接崩溃。

extern "C" _declspec(dllexport) int Add(int x, int y)
{
	char *p = nullptr;
	*p = '1';  // 此处会抛异常 

	return x + y;
}
[DllImport("MyDll")]
public static extern int Add(int x, int y);

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    Add(1, 2);
}

结合前面 Task 内部异常的特性,可以将调用代码放在 Task 中,以避免程序崩溃。Task 是个神奇的东西,还需要进一步深入学习。

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    Task.Run(() => Add(1, 2));
}

你可能感兴趣的:(C#/WinForm/WPF,.NET)