我们写UWP应用的时候难免遇到未处理的异常,不然你的应用就会在用户面前闪退,非常没有逼格。然而Windows.UI.Xaml.Application的UnhandledException事件里面有个巨坑,就是它不能处理async异步方法里的异常。注释里也没提到这回事:

//
// Summary:
//     Occurs when an exception can be handled by app code, as forwarded from a native-level
//     Windows Runtime error. Apps can mark the occurrence as handled in event data.
public event UnhandledExceptionEventHandler UnhandledException;

处理全局异常确实是用这个事件没错,但是我们需要用一个线程同步的方法来搞,已经有人帮我们写好了,代码看起来逼格侧漏,无脑拷代码就行,给我们的应用增加一点逼格:

来自:https://github.com/kiwidev/WinRTExceptions

using System;
using System.Threading;
using Windows.UI.Xaml.Controls;

namespace ShanghaiMetro.Core
{
    /// <summary>
    /// Wrapper around a standard synchronization context, that catches any unhandled exceptions.
    /// Acts as a facade passing calls to the original SynchronizationContext
    /// </summary>
    /// <example>
    /// Set this up inside your App.xaml.cs file as follows:
    /// <code>
    /// protected override void OnActivated(IActivatedEventArgs args)
    /// {
    ///     EnsureSyncContext();
    ///     ...
    /// }
    /// 
    /// protected override void OnLaunched(LaunchActivatedEventArgs args)
    /// {
    ///     EnsureSyncContext();
    ///     ...
    /// }
    /// 
    /// private void EnsureSyncContext()
    /// {
    ///     var exceptionHandlingSynchronizationContext = ExceptionHandlingSynchronizationContext.Register();
    ///     exceptionHandlingSynchronizationContext.UnhandledException += OnSynchronizationContextUnhandledException;
    /// }
    /// 
    /// private void OnSynchronizationContextUnhandledException(object sender, UnhandledExceptionEventArgs args)
    /// {
    ///     args.Handled = true;
    /// }
    /// </code>
    /// </example>
    public class ExceptionHandlingSynchronizationContext : SynchronizationContext
    {
        /// <summary>
        /// Registration method.  Call this from OnLaunched and OnActivated inside the App.xaml.cs
        /// </summary>
        /// <returns></returns>
        public static ExceptionHandlingSynchronizationContext Register()
        {
            var syncContext = Current;
            if (syncContext == null)
                throw new InvalidOperationException("Ensure a synchronization context exists before calling this method.");


            var customSynchronizationContext = syncContext as ExceptionHandlingSynchronizationContext;


            if (customSynchronizationContext == null)
            {
                customSynchronizationContext = new ExceptionHandlingSynchronizationContext(syncContext);
                SetSynchronizationContext(customSynchronizationContext);
            }


            return customSynchronizationContext;
        }

        /// <summary>
        /// Links the synchronization context to the specified frame
        /// and ensures that it is still in use after each navigation event
        /// </summary>
        /// <param name="rootFrame"></param>
        /// <returns></returns>
        public static ExceptionHandlingSynchronizationContext RegisterForFrame(Frame rootFrame)
        {
            if (rootFrame == null)
                throw new ArgumentNullException(nameof(rootFrame));

            var synchronizationContext = Register();

            rootFrame.Navigating += (sender, args) => EnsureContext(synchronizationContext);
            rootFrame.Loaded += (sender, args) => EnsureContext(synchronizationContext);

            return synchronizationContext;
        }

        private static void EnsureContext(SynchronizationContext context)
        {
            if (Current != context)
                SetSynchronizationContext(context);
        }


        private readonly SynchronizationContext _syncContext;


        public ExceptionHandlingSynchronizationContext(SynchronizationContext syncContext)
        {
            _syncContext = syncContext;
        }


        public override SynchronizationContext CreateCopy()
        {
            return new ExceptionHandlingSynchronizationContext(_syncContext.CreateCopy());
        }


        public override void OperationCompleted()
        {
            _syncContext.OperationCompleted();
        }


        public override void OperationStarted()
        {
            _syncContext.OperationStarted();
        }


        public override void Post(SendOrPostCallback d, object state)
        {
            _syncContext.Post(WrapCallback(d), state);
        }


        public override void Send(SendOrPostCallback d, object state)
        {
            _syncContext.Send(d, state);
        }


        private SendOrPostCallback WrapCallback(SendOrPostCallback sendOrPostCallback)
        {
            return state =>
            {
                try
                {
                    sendOrPostCallback(state);
                }
                catch (Exception ex)
                {
                    if (!HandleException(ex))
                        throw;
                }
            };
        }

        private bool HandleException(Exception exception)
        {
            if (UnhandledException == null)
                return false;

            var exWrapper = new UnhandledExceptionEventArgs
            {
                Exception = exception
            };

            UnhandledException(this, exWrapper);

#if DEBUG && !DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
            if (System.Diagnostics.Debugger.IsAttached) System.Diagnostics.Debugger.Break();
#endif

            return exWrapper.Handled;
        }


        /// <summary>
        /// Listen to this event to catch any unhandled exceptions and allow for handling them
        /// so they don't crash your application
        /// </summary>
        public event EventHandler<UnhandledExceptionEventArgs> UnhandledException;
    }

    public class UnhandledExceptionEventArgs : EventArgs
    {
        public bool Handled { get; set; }
        public Exception Exception { get; set; }
    }
}

现在,打开App.xaml.cs把逼装完:

public App()
{
    this.InitializeComponent();
    ...
    // https://github.com/kiwidev/WinRTExceptions
    this.UnhandledException += OnUnhandledException;
}

注意这里的UnhandledExceptionEventArgs的类型是Windows.UI.Xaml.UnhandledExceptionEventArgs不是我们自定义的ShanghaiMetro.Core.UnhandledExceptionEventArgs。这里处理的是同步的情况。

private async void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
{
    e.Handled = true;
    await new MessageDialog("Application Unhandled Exception:\r\n" + e.Exception.Message, "爆了 :(")
        .ShowAsync();
}

对于异步方法里的异常,需要再写个方法

/// <summary>
/// Should be called from OnActivated and OnLaunched
/// </summary>
private void RegisterExceptionHandlingSynchronizationContext()
{
    ExceptionHandlingSynchronizationContext
        .Register()
        .UnhandledException += SynchronizationContext_UnhandledException;
}

private async void SynchronizationContext_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    e.Handled = true;
    await new MessageDialog("Synchronization Context Unhandled Exception:\r\n" + e.Exception.Message, "爆了 :(")
        .ShowAsync();
}

然后记得在OnActivated和OnLaunched事件里把逼装完:

protected override async void OnLaunched(LaunchActivatedEventArgs e)
{
    RegisterExceptionHandlingSynchronizationContext();
	...
}
protected override async void OnActivated(IActivatedEventArgs args)
{
    RegisterExceptionHandlingSynchronizationContext();
	...
}

现在这个逼装的基本差不多了,一旦我们的应用程序有异常,不管是同步的还是异步的,都会弹一个框出来而不是闪退。

但是,为了调试方便,我们通常还需要带上堆栈信息,然而问题来了,异步的堆栈信息长这样:

来自 https://github.com/ljw1004/async-exception-stacktrace

  at VB$StateMachine_3_BarAsync.MoveNext() ~~in Class1.vb:line 24~~
--- End of stack trace from previous location where exception was thrown ---
at TaskAwaiter.ThrowForNonSuccess(Task task)
  at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at TaskAwaiter.GetResult()
  at VB$StateMachine_2_FooAsync.MoveNext() ~~in Class1.vb:line 19~~
--- End of stack trace from previous location where exception was thrown ---
  at TaskAwaiter.ThrowForNonSuccess(Task task)
  at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
  at TaskAwaiter.GetResult()
  at VB$StateMachine_1_TestAsync.MoveNext() ~~in Class1.vb:line 14~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter`1.GetResult()
   at VB$StateMachine_0_Button1_Click.MoveNext() ~~in Class1.vb:line 5 ~~
   at VB$StateMachine_3_BarAsync.MoveNext() ~~in Class1.vb:line 24~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter.GetResult()
   at VB$StateMachine_2_FooAsync.MoveNext() ~~in Class1.vb:line 19~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter.GetResult()
   at VB$StateMachine_1_TestAsync.MoveNext() ~~in Class1.vb:line 14~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter`1.GetResult()
   at VB$StateMachine_0_Button1_Click.MoveNext() ~~in Class1.vb:line 5~~

我们希望简单粗暴明了,最好是这样:

   at Test.BarAsync
   at Test.FooAsync()#BarAsync in Class1.vb:19
   at Test.TestAsync()#FooAsync(True) in Class1.vb:14
   at Test.Button1_Click() in Class1.vb:5 

幸运的是,这个nuget包可以帮助我们实现这样简短的堆栈信息:https://www.nuget.org/packages/AsyncStackTraceEx/

安装之后,就可以写个小方法:

// https://github.com/ljw1004/async-exception-stacktrace
private string GetExceptionDetailMessage(Exception ex)
{
    return $"{ex.Message}\r\n{ex.StackTraceEx()}";
}

然后把刚才那两个弹框的信息给改下:

await new MessageDialog("Synchronization Context Unhandled Exception:\r\n" + GetExceptionDetailMessage(e.Exception), "爆了 :(")
    .ShowAsync();

现在这个逼就装完了,一旦有异常,就会看到这样的画面:

如果你希望用户反馈问题方便一点,可以结合这篇《Windows 10 UWP开发:报错和反馈页面的实现》把异常信息通过邮件发送给应用作者。