Why Is Faulty Behaviour In The .NET Framework Not Fixed?
- by Alois Kraus
Here is the scenario: You have a Windows Form Application that calls a method via Invoke or BeginInvoke which throws exceptions. Now you want to find out where the error did occur and how the method has been called. Here is the output we do get when we call Begin/EndInvoke or simply Invoke The actual code that was executed was like this: private void cInvoke_Click(object sender, EventArgs e)
{
InvokingFunction(CallMode.Invoke);
}
[MethodImpl(MethodImplOptions.NoInlining)]
void InvokingFunction(CallMode mode)
{
switch (mode)
{
case CallMode.Invoke:
this.Invoke(new MethodInvoker(GenerateError));
The faulting method is called GenerateError which does throw a NotImplementedException exception and wraps it in a NotSupportedException.
[MethodImpl(MethodImplOptions.NoInlining)]
void GenerateError()
{
F1();
}
private void F1()
{
try
{
F2();
}
catch (Exception ex)
{
throw new NotSupportedException("Outer Exception", ex);
}
}
private void F2()
{
throw new NotImplementedException("Inner Exception");
}
It is clear that the method F2 and F1 did actually throw these exceptions but we do not see them in the call stack. If we directly call the InvokingFunction and catch and print the exception we can find out very easily how we did get into this situation. We see methods F1,F2,GenerateError and InvokingFunction directly in the stack trace and we see that actually two exceptions did occur.
Here is for comparison what we get from Invoke/EndInvoke
System.NotImplementedException: Inner Exception
StackTrace: at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
at WindowsFormsApplication1.AppForm.InvokingFunction(CallMode mode)
at WindowsFormsApplication1.AppForm.cInvoke_Click(Object sender, EventArgs e)
at System.Windows.Forms.Control.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnClick(EventArgs e)
The exception message is kept but the stack starts running from our Invoke call and not from the faulting method F2. We have therefore no clue where this exception did occur! The stack starts running at the method MarshaledInvoke because the exception is rethrown with the throw catchedException which resets the stack trace.
That is bad but things are even worse because if previously lets say 5 exceptions did occur .NET will return only the first (innermost) exception. That does mean that we do not only loose the original call stack but all other exceptions and all data contained therein as well.
It is a pity that MS does know about this and simply closes this issue as not important. Programmers will play a lot more around with threads than before thanks to TPL, PLINQ that do come with .NET 4. Multithreading is hyped quit a lot in the press and everybody wants to use threads. But if the .NET Framework makes it nearly impossible to track down the easiest UI multithreading issue I have a problem with that. The problem has been reported but obviously not been solved. .NET 4 Beta 2 did not have changed that dreaded GetBaseException call in MarshaledInvoke to return only the innermost exception of the complete exception stack. It is really time to fix this.
WPF on the other hand does the right thing and wraps the exceptions inside a TargetInvocationException which makes much more sense. But Not everybody uses WPF for its daily work and Windows forms applications will still be used for a long time.
Below is the code to repro the issues shown and how the exceptions can be rendered in a meaningful way. The default Exception.ToString implementation generates a hard to interpret stack if several nested exceptions did occur.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Globalization;
using System.Runtime.CompilerServices;
namespace WindowsFormsApplication1
{
public partial class AppForm : Form
{
enum CallMode
{
Direct = 0,
BeginInvoke = 1,
Invoke = 2
};
public AppForm()
{
InitializeComponent();
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
}
void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
cOutput.Text = PrintException(e.Exception, 0, null).ToString();
}
private void cDirectUnhandled_Click(object sender, EventArgs e)
{
InvokingFunction(CallMode.Direct);
}
private void cDirectCall_Click(object sender, EventArgs e)
{
try
{
InvokingFunction(CallMode.Direct);
}
catch (Exception ex)
{
cOutput.Text = PrintException(ex, 0, null).ToString();
}
}
private void cInvoke_Click(object sender, EventArgs e)
{
InvokingFunction(CallMode.Invoke);
}
private void cBeginInvokeCall_Click(object sender, EventArgs e)
{
InvokingFunction(CallMode.BeginInvoke);
}
[MethodImpl(MethodImplOptions.NoInlining)]
void InvokingFunction(CallMode mode)
{
switch (mode)
{
case CallMode.Direct:
GenerateError();
break;
case CallMode.Invoke:
this.Invoke(new MethodInvoker(GenerateError));
break;
case CallMode.BeginInvoke:
IAsyncResult res = this.BeginInvoke(new MethodInvoker(GenerateError));
this.EndInvoke(res);
break;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
void GenerateError()
{
F1();
}
private void F1()
{
try
{
F2();
}
catch (Exception ex)
{
throw new NotSupportedException("Outer Exception", ex);
}
}
private void F2()
{
throw new NotImplementedException("Inner Exception");
}
StringBuilder PrintException(Exception ex, int identLevel, StringBuilder sb)
{
StringBuilder builtStr = sb;
if( builtStr == null )
builtStr = new StringBuilder();
if( ex == null )
return builtStr;
WriteLine(builtStr, String.Format("{0}: {1}", ex.GetType().FullName, ex.Message), identLevel);
WriteLine(builtStr, String.Format("StackTrace: {0}", ShortenStack(ex.StackTrace)), identLevel + 1);
builtStr.AppendLine();
return PrintException(ex.InnerException, ++identLevel, builtStr);
}
void WriteLine(StringBuilder sb, string msg, int identLevel)
{
foreach (string trimmedLine in SplitToLines(msg)
.Select( (line) => line.Trim()) )
{
for (int i = 0; i < identLevel; i++)
sb.Append('\t');
sb.Append(trimmedLine);
sb.AppendLine();
}
}
string ShortenStack(string stack)
{
int nonAppFrames = 0;
// Skip stack frames not part of our app but include two foreign frames and skip the rest
// If our stack frame is encountered reset counter to 0
return SplitToLines(stack)
.Where((line) =>
{
nonAppFrames = line.Contains("WindowsFormsApplication1") ? 0 : nonAppFrames + 1;
return nonAppFrames < 3;
})
.Select((line) => line)
.Aggregate("", (current, line) => current + line + Environment.NewLine);
}
static char[] NewLines = Environment.NewLine.ToCharArray();
string[] SplitToLines(string str)
{
return str.Split(NewLines, StringSplitOptions.RemoveEmptyEntries);
}
}
}