This is admittedly a hack, but it's the best solution I was able to come up with which supports both global exception handling in WinForms and all exceptions, even with inner exceptions.
In the Program.cs
:
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
Application.ThreadException += Application_ThreadException;
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, true);
Application.Run(new MyMainForm());
}
private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
{
_outermostExceptionCache.AddException(e.Exception);
}
private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
{
Exception exception = null;
if (e?.Exception != null)
exception = _outermostExceptionCache.GetOutermostException(e.Exception);
// Handle exception
}
private static OutermostExceptionCache _outermostExceptionCache = new();
}
And for that you'll need the OutermostExceptionCache
class:
public class OutermostExceptionCache
{
public void AddException(Exception ex)
{
if ((ex != null) && (ex is not TargetInvocationException))
{
Exception innermostException = GetInnermostException(ex);
lock (_syncRoot)
{
RemoveOldEntries();
_cache[innermostException] = new CacheEntry(ex);
}
}
}
public Exception GetOutermostException(Exception ex)
{
Exception innermostException = GetInnermostException(ex);
Exception outermostException = null;
lock (_syncRoot)
{
if (_cache.TryGetValue(innermostException, out CacheEntry entry))
{
outermostException = entry.Exception;
_cache.Remove(innermostException);
}
else
{
outermostException = ex;
}
}
return outermostException;
}
private void RemoveOldEntries()
{
DateTime now = DateTime.Now;
foreach (KeyValuePair<Exception, CacheEntry> pair in _cache)
{
TimeSpan timeSinceAdded = now - pair.Value.AddedTime;
if (timeSinceAdded.TotalMinutes > 3)
_cache.Remove(pair.Key);
}
}
private Exception GetInnermostException(Exception ex)
{
return ex.GetBaseException() ?? ex;
}
private readonly object _syncRoot = new();
private readonly Dictionary<Exception, CacheEntry> _cache = new();
private class CacheEntry
{
public CacheEntry(Exception ex)
{
Exception = ex;
AddedTime = DateTime.Now;
}
public Exception Exception { get; }
public DateTime AddedTime { get; }
}
}
The way this works is by watching every exception, as it is thrown, before the runtime even bubbles the exception up to the nearest catch block. Each time an exception is thrown, it is added to a cache, indexed by the innermost (i.e. base) exception. Therefore, when an exception is caught and a new exception is thrown, with the original one as its inner exception, the cache is updated with that outer exception. Then, when Application.ThreadException
event handler is provided with the unwrapped, innermost, exception, the handler can look up the outermost one from the cache.
Note: Since even locally-caught exceptions will get added to the cache (and therefore never removed via a call to GetOutermostException
), it timestamps each one and automatically ditches any that are older than 3 minutes. That's an arbitrary timeout which can be adjusted as needed. If you make the timeout too short, it could cause problems with debugging since it can cause the exception handling to revert to handling only the innermost exception if you pause the process too long in the debugger (after the exception is thrown but before it is handled).