开发者

InvokeRequired Exception Handling

I noticed a few strange behaviors in a Windows F开发者_JAVA百科orms scenario which involves threads and UI, so, naturally, this means making use of the InvokeRequired property. The situation: my application makes use of a thread to do some work and the thread sends an event into the UI. The UI displays a message based on an Internationalization system which consists of a dictionary with keys. The I18N system cannot find a key in the dictionary and crashes.

Notes: application is in Debug Mode and I have a try-catch over the entire "Application.Run();" back in Program.cs. However, that try-catch is not reached, as what I will discuss here is based on inner Exception handling, but I mentioned it just in case.

So now here comes the fun parts:

  1. Why, for the life of me, does Visual Studio "censor" exception information from me? In the code below, you will see on the if (InvokeRequired) branch, a try-catch. I log the exception. ex.InnerException is NULL and the provided ex.StackTrace is anemic (only 1 step in it). Now if I comment the try-catch and simply let it crash via the Debugger, I get a much ampler stack trace. Why is that?

  2. To make things worse, neither of the two stack traces versions contain any information about the i18N crash. They just say "The given key was not present in the dictionary." and give me a stack trace up to the Invoke declaration.

  3. On the else branch (that is, InvokeRequired == false), if I put a try-catch, I can successfully catch my Exception back to the i18n system. As you can see, I tried to send my exception with InnerException back to the InvokeRequired == true branch. However, even so, InnerException stays NULL there and I cannot access my i18N error.

I am puzzled by all these things and maybe somebody can help shed some light over here. If you got really strong lanterns that is.

Here is the function's code.

private delegate void AddMessageToConsole_DELEGATE (frmMainPresenter.PresenterMessages message);
private void AddMessageToConsole (frmMainPresenter.PresenterMessages message)
{
  if (InvokeRequired)
  { //Catching any errors that occur inside the invoked function.
    try { Invoke(new AddMessageToConsole_DELEGATE(AddMessageToConsole), message); }
    catch (Exception ex) { MSASession.ErrorLogger.Log(ex); }
    //Invoke(new AddMessageToConsole_DELEGATE(AddMessageToConsole), message);
  }
  else
  {
    string message_text = ""; //Message that will be displayed in the Console / written in the Log.
    try
    {
      message_text = I18N.GetTranslatedText(message)
    }
    catch (Exception ex)
    {
      throw new Exception(ex.Message, ex);
    }

    txtConsole.AppendText(message_text);
  }
}


Yes, this is built-in behavior for Control.Invoke(). It only marshals the deepest nested InnerException back to the caller. Not so sure why they did this, beyond avoiding reporting exceptions that were raised by the marshaling code and would confuzzle the reader. It was done explicitly, you cannot change the way it works.

But keep your eyes on the ball, the real problem is that the string indeed cannot be found in the dictionary. The reason for that is that your background thread runs with a different culture from your UI thread. Different cultures have different string comparison rules. You either need to give your dictionary a different comparator (StringComparer.InvariantCulture) or you should switch your background thread to the same culture as your UI thread.

Dealing with a non-system default culture in your UI thread can be difficult, all other threads will be started with the system default. Especially threadpool threads are troublesome, you don't always control how they get started. And culture is not part of the Thread.ExecutionContext so doesn't get forwarded. This can cause subtle bugs, like the one you ran into. Other nastiness is, say, SortedList which suddenly becomes unsorted when read by a thread that uses a different culture. Using the system default culture is strongly recommended. Its what your user is likely to use anyway.


The call stack problem is a known issue with Control.Invoke. You lose the call stack. Sorry. This is because it is rethrown on the UI thread using throw ex;.

The best solution would be to replace the background thread with a background Task. Note: this solution is only available for .NET 4.0. The Task class properly marshals exceptions. I wrote a blog entry about reporting progress from tasks, and the code in that blog entry will allow you to catch any UI update errors in the background thread, preserving the original exception and its call stack.

If you can't upgrade to .NET 4.0 yet, there is a workaround. Microsoft's Rx library includes a CoreEx.dll which has an extension method for Exception called PrepareForRethrow. This is supported in .NET 3.5 SP1 and .NET 4.0 (and SL 3 and SL 4). You'll need to wrap your UI updater method with something a little uglier:

private delegate void AddMessageToConsole_DELEGATE (frmMainPresenter.PresenterMessages message); 
private void AddMessageToConsole (frmMainPresenter.PresenterMessages message) 
{ 
  if (InvokeRequired) 
  {
    // Invoke the target method, capturing the exception.
    Exception ex = null;
    Invoke((MethodInvoker)() =>
    {
       try
       {
         AddMessageToConsole(message);
       }
       catch (Exception error)
       {
         ex = error;
       }
    });

    // Handle error if it was thrown
    if (ex != null)
    {
      MSASession.ErrorLogger.Log(ex);

      // Rethrow, preserving exception stack
      throw ex.PrepareForRethrow();
    }
  } 
  else 
  { 
    string message_text = ""; //Message that will be displayed in the Console / written in the Log. 
    try 
    { 
      message_text = I18N.GetTranslatedText(message) 
    } 
    catch (Exception ex) 
    { 
      throw new Exception(ex.Message, ex); 
    } 

    txtConsole.AppendText(message_text); 
  } 
} 

Note: I recommend you start a migration away from ISynchronizeInvoke. It is an outdated interface that is not carried forward into newer UI frameworks (e.g., WPF, Silverlight). The replacement is SynchronizationContext, which supports WinForms, WPF, Silverlight, ASP.NET, etc. SynchronizationContext is much more suitable as an abstract "thread context" for a business layer.


Invoke on a Windows.Forms object causes the function to be invoked on a separate thread. If an Exception is thrown in your invoked function, the Exception is caught and a new TargetInvocationException is thrown.

This TargetInvocationException contains the initial Excpetion in it's InnerException property.

So, try to do it this way:

catch (TargetInvocationException ex) { MSASession.ErrorLogger.Log(ex.InnerException); }

Edit: Also, if you expand the InnerException property in the debugger, you will be able to access it's stacktrace, even if only as plain text.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜