-->

Exception handling with multiple forms

2019-03-11 07:19发布

问题:

I'm seeing different behavior with exceptions being caught or not being caught when I am debugging vs. when I am running a compiled .exe. I have two forms (Form1 and Form2). Form1 has a button on it which instantiates and calls ShowDialog on Form2. Form2 has a button on it which intentionally produces a divide by zero error. When I'm debugging, the catch block in Form1 is hit. When I run the compiled .exe, it is NOT hit, and instead I get a message box that states, "Unhandled exception has occurred in your application. If you click continue, the application will ignore this error and attempt to continue. If you click Quit, the application will close immediately...Attempted to divide by zero". My question is why do you get different behavior when debugging vs. when running the .exe? If that is the expected behavior, then would it be considered necessary to put try/catch blocks in every single event handler? That seems kind of crazy over kill, doesn't it?

Here's the code for Form1.

public partial class Form1 : Form
{
    public Form1()
    {
            InitializeComponent();

    }

    private void button1_Click(object sender, EventArgs e)
    {
        try
        {
            Form2 f2 = new Form2();
            f2.ShowDialog();
        }
        catch(Exception eX)
        {
            MessageBox.Show( eX.ToString()); //This line hit when debugging only
        }
    }
}

Here's Form2's code:

public partial class Form2 : Form
{
    public Form2()
    {
            InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
            int x = 0;
            int y = 7 / x;

    }
}

回答1:

I get the same behaviour as you. I don't know why this happens, but it seems to be a bad idea to assume that an exception generated from an event in a form will appear on the stack of the ShowDialog() call. It would be better to do these two things:

  • Catch and handle the exceptions in the event handlers in Form2 where it makes sense to do so, and when you can do something meaningful with the exception.
  • Add an unhandled exception handler (`Application_ThreadException`) for your entire application to catch any unhandled exceptions.

Update: Here are the stack traces. Debug version:

System.DivideByZeroException: Attempted to divide by zero.
   at WindowsFormsApplication1.Form2.button1_Click(Object sender, EventArgs e) in ...\WindowsFormsApplication1\Form2.cs:line 27
   at System.Windows.Forms.Control.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
   at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.ButtonBase.WndProc(Message& m)
   at System.Windows.Forms.Button.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
   at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
   at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.RunDialog(Form form)
   at System.Windows.Forms.Form.ShowDialog(IWin32Window owner)
   at System.Windows.Forms.Form.ShowDialog()
   at WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) in ...\WindowsFormsApplication1\Form1.cs:line 45

Release:

System.DivideByZeroException: Attempted to divide by zero.
   at WindowsFormsApplication1.Form2.button1_Click(Object sender, EventArgs e) in ...\WindowsFormsApplication1\Form2.cs:line 27
   at System.Windows.Forms.Control.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
   at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.ButtonBase.WndProc(Message& m)
   at System.Windows.Forms.Button.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

Notice that System.Windows.Forms.Form.ShowDialog() is not in the stack trace in release mode, which is why your try {} catch {} does nothing. Also notable is that in the debug case it is using NativeWindow.DebuggableCallback which is presumably designed to help debugging by not breaking the stack, whereas in Release mode is uses NativeWindow.Callback.



回答2:

Yes, this is by design and is closely associated with the way Windows Forms works. In a Winforms app, code runs in response to messages posted to the active window by Windows. Every native Windows app contains a message loop to detect these messages. The Winforms plumbing ensures one of your event handlers runs in response; button1_Click in your example code.

Most Winforms controls implement their own event handlers. A PictureBox for example has a Paint event handler that ensures its Image is drawn to the screen. This is all done automatically, you don't have to write any code yourself to make this work.

There is however a problem when this code throws an exception, there is no way for you to catch such an exception since none of your own code was involved. In other words, there is no place for you to inject your own try block. The very last bit of your own program's code that was involved is the code that got the message loop started. The Application.Run() method call, normally in Program.cs. Or the Form.ShowDialog() call if you display a dialog. Either of those methods start a message loop. Putting a try block around the Application.Run() call isn't useful, the app will terminate after catching an exception.

To combat this problem, the Winforms message loop code contains a try block around the code that dispatches an event. Its catch clause displays the dialog you mentioned, it is implemented by the ThreadExceptionDialog class.

Getting to the point of your question: this catch clause really gets in the way of troubleshooting problems with your code when you debug. The debugger will only stop on an exception when there is no catch block that handles the exception. But when your code throws an exception, you'll want to know about it when you debug. The previously mentioned code in the message loop is aware whether or not a debugger is attached. If it is, it dispatches events without the try/catch block. Now, when your code throws an exception, there is no catch clause to handle it and the debugger will stop the program, giving you a chance to find out what went wrong.

Perhaps you see now why your program behaves the way it does. When you debug, the catch clause in the message loop is disabled, giving the catch clause in the Form1 code a chance to catch the exception. When you don't, the message loop catch clause handles the exception (by displaying the dialog) and prevents the exception from unwinding to the Form1 code.

You can prevent the message loop catch clause from being used at all by calling the Application.SetUnhandledExceptionMode() method, passing UnhandledExceptionMode.ThrowException. Do so in the Main() method, before the Application.Run() call. Now your program will behave the same either way.

This is in general not a bad idea, giving the user the option to Continue in the exception dialog is a questionable feature. Do implement an event handler for AppDomain.UnhandledException event in that case so there's at least some diagnostic to the user.