我们写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开发:报错和反馈页面的实现》把异常信息通过邮件发送给应用作者。