防止未处理的异常对话框出现

3

首先,让我说一下我已经彻底阅读了this useful article并使用CodeProject中的SafeThread类。使用Thread或SafeThread时,我得到的结果是一样的。

我将我的问题缩小到一个包含两个表单(每个表单都有一个按钮)的应用程序。主程序显示一个表单。当你点击该按钮时,会启动一个新的线程,显示第二个表单。当你在第二个表单上点击按钮时,它内部只是执行 "throw new Exception()"。

当我在VS2008下运行这个应用程序时,我看到 "Exception in DoRun()"。

当我在VS2008之外运行时,我会看到一个对话框,显示 "Unhandled exception has occurred in your application. If you click continue, the application ...."

我尝试在app.config中将legacyUnhandledExceptionPolicy设置为1和0。

当不在VS2008下运行时,我需要做什么来捕获在我的第二个表单中抛出的异常?

这是我的Program.cs

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.ThreadException += new ThreadExceptionEventHandler    (Application_ThreadException);
            Application.SetUnhandledExceptionMode    (UnhandledExceptionMode.CatchException);
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            try
            {
               Application.Run(new Form1());
            }
            catch(Exception ex)
            {
                MessageBox.Show("Main exception");
            }                
        }

        static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            MessageBox.Show("CurrentDomain_UnhandledException");
        }

        static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
        {
            MessageBox.Show("Application_ThreadException");
        }
    }

这是表单1:

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

    private void button1_Click(object sender, EventArgs e)
    {
        SafeThread t = new SafeThread(new SimpleDelegate(ThreadMain));
        try
        {
            t.ShouldReportThreadAbort = true;
            t.ThreadException += new ThreadThrewExceptionHandler(t_ThreadException);
            t.ThreadCompleted += new ThreadCompletedHandler(t_ThreadCompleted);
            t.Start();
        }
        catch(Exception ex)
        {
            MessageBox.Show(string.Format("Caught externally! {0}", ex.Message));

        }
    }

    void t_ThreadCompleted(SafeThread thrd, bool hadException, Exception ex)
    {
        MessageBox.Show("t_ThreadCompleted");
    }

    void t_ThreadException(SafeThread thrd, Exception ex)
    {
        MessageBox.Show(string.Format("Caught in safe thread! {0}", ex.Message));
    }

    void ThreadMain()
    {
        try
        {
            DoRun();
        }
        catch (Exception ex)
        {
            MessageBox.Show(string.Format("Caught! {0}", ex.Message));
        }
    }

    private void DoRun()
    {
        try
        {
            Form2 f = new Form2();
            f.Show();
            while (!f.IsClosed)
            {
                Thread.Sleep(1);
                Application.DoEvents();
            }
        }
        catch(Exception ex)
        {
            MessageBox.Show("Exception in DoRun()");
        }
    }
}

这里是 Form2:

public partial class Form2 : Form
{
    public bool IsClosed { get; private set; }

    public Form2()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        throw new Exception("INTERNAL EXCEPTION");
    }

    protected override void OnClosed(EventArgs e)
    {
        IsClosed = true;
    }
}

你确切想要什么样的行为? - scwagner
异常中的堆栈跟踪是什么? - Jeremy McGee
我想让我的一个异常处理程序捕获在Form2中button1_Click抛出的异常。这在VS2008中发生,但在外部却没有。以下是堆栈跟踪的顶部 在C:\ codefarm \ Trapper \ Form2.cs的Trapper.Form2.button1_Click(Object sender,EventArgs e):第17行 在System.Windows.Forms.Control.OnClick(EventArgs e) 在System.Windows.Forms.Button.OnClick(EventArgs e) - rc1
5个回答

3
如果您真的想要在单独的UI线程上打开第二个表格(不是使用ShowDialog())以捕获异常并将其发送到您的Application_ThreadException方法中,您需要确保第二个线程也设置为CatchException并且您需要在该线程上订阅Application.ThreadException。这两者都是特定于线程的(有点古怪)。
您可以通过调用以下内容来设置默认的"未处理异常模式":
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, false);

这将为您创建的任何UI线程设置应用程序级别的CatchException模式。(但在Visual Studio调试器和其他某些情况下,此调用将失败。)或者,您的新UI线程可以使用通常的调用(与此重载传递true相同)来设置自己的模式。

无论哪种方式,新的UI线程也需要订阅Application.ThreadException事件本身,因为订阅者存储在[ThreadStatic]变量中。

Application.ThreadException += Program.Application_ThreadException;

如果有必要,它可以使用单独的处理程序而不是路由到相同的处理程序。

我不确定这如何与使用 SafeThread 交叉来完成,但我认为如果第二个UI线程正确执行此操作,则不需要使用 SafeThread 。 这很像您在主UI线程上执行的操作。

还可以查看我的答案此问题以获取更多关于此内容的技巧。


Application.ThreadException += Program.Application_ThreadException; 事件处理程序运行良好,我在 Program.cs 中放置了一个事件处理程序,当发生未处理的异常时它会触发,谢谢! - mili

2

1.) 我建议使用BackgroundWorker而不是像这样单独使用线程。您的worker将捕获异常并将其作为参数传递给完成处理程序。

2.) 当显示第二个窗体时,我建议使用ShowDialog()而不是Show(),这将阻止DoRun()在该方法调用处,并且异常应该会被您周围的try / catch(或者如果您使用BackgroundWorker)捕获。

我认为问题在于,由于您正在调用Show(),因此实质上正在将该调用分派到Invoker中,这最终将排队在UI线程中。因此,当异常发生时,没有更高层次的调用堆栈来捕获它。我相信调用ShowDialog()将解决此问题(还可以让您放弃那个可怕的for循环)。

类似于以下内容:

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

    private void button1_Click(object sender, EventArgs e)
    {
        // NOTE: I forget the event / method names, these are probably a little wrong.
        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += (o, e) =>
        {
            Form2 f = new Form2();
            e.Result = f.ShowDialog();
        };
        worker.DoWorkComplete += (o, e) =>
        { 
            if(e.Error != null)
                MessageBox.Show(string.Format("Caught Error: {0}", ex.Message));

            // else success!
            // use e.Result to figure out the dialog closed result.
        };

        worker.DoWorkAsync();
    }
}

实际上,现在我想起来了,从后台线程打开对话框有点奇怪,但我认为这仍然可以正常工作。


是的,从应用程序的主线程以外的线程打开对话框不仅很奇怪,而且很危险。这就像从非 UI 线程更改 UI 一样。它可能会在运行时出现问题。如果您需要基于其他线程中的某些内容显示对话框,则需要从该线程发送事件返回到 UI 线程,然后可以显示对话框。 - jasonh
当我说“将事件发送回UI线程”时,我的意思是工作线程应该以某种方式导致UI线程上的表单或控件的Invoke或BeginInvoke方法被调用。你想要如何做到这一点(我使用一个事件,然后Form有一个方法,在直接调用显示对话框的方法之前检查InvokeRequired或在该方法上调用Invoke/BeginInvoke)由你决定。 - jasonh

1

用这行代码替代:

Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

你需要这个:
#if DEBUG
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
#else
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
#endif

这样,当您在Visual Studio下以调试模式运行程序时,Visual Studio将在异常发生时捕获它们,以便您可以在发生异常的点进行调试。当您以发布模式运行程序时,异常将被Application.ThreadException处理程序或AppDomain处理程序捕获。

这在我的程序中完美地运作。我厌倦了收到带有“应用程序中发生未处理的异常…”框的电子邮件,因此我实现了一个通用表单,其中包含一个文本框,允许我转储特定信息,以便我用于调试问题。


为什么不行?发生了什么事?我有一个工作中的应用程序(使用普通的线程),如果线程中抛出未被捕获的异常,全局处理程序会接管它。 - jasonh
1
你是不是把这两行反了?你的文本表明你想让发布模式使用Application.ThreadException,这需要CatchException模式(仅适用于UI线程)。如果你将模式设置为ThrowException(如果这是你想要的),调试器会停在未处理的异常上。你可能还想根据Debugger.IsAttached而不是#if DEBUG进行关键操作。或者,你可以让它CatchException,在Visual Studio中:Debug->Exceptions...对话框中勾选当CLR异常被抛出时中断的复选框(等等)。 - Rob Parker

0
你尝试在主方法中加入try/catch块了吗?

是的,这没有任何影响(已更新代码清单以反映此更改)。 - rc1

0
问题似乎是由按钮事件处理程序引起的。如果在DoRun()中抛出异常 - 可能是在Form.Show()之后 - 无论您是否在Visual Studio内运行,都会像预期的那样捕获异常。
有趣的是,行为取决于调试器是否附加到进程上。从Visual Studio外部启动并稍后附加调试器可以防止发送反馈消息框,而分离则会再次发生。从Visual Studio内部也是如此 - “开始而不调试”会导致发送反馈消息框出现。
因此,在按钮事件处理程序中发生异常后,我快速地浏览了框架源代码,并粗略地查看了它 - 消息泵、控件和可能有很多其他代码在那里执行很多操作。因为WinForms只是本机控件的包装器,所以我假设由于某种原因,无论调试器是否附加,异常都没有返回到相同的点或线程 - 可能是当异常跨越线程或进程边界传递时出了问题之类的事情。

这里发生的情况是 UnhandledExceptionMode.CatchException 允许任何东西在代码运行时捕获异常,中断您设置的链。当 VS 被附加时,它恰好是 Visual Studio 本身。当它没有附加时,.NET Framework 将首先处理该异常,并通过弹出那个令人恼火且无用的对话框来处理该异常。通过将其设置为 UnhandledExceptionMode.ThrowException,异常将继续向上冒泡到您的应用程序线程异常或 AppDomain 处理程序,就像您所期望的那样。 - jasonh
UnhandledExceptionMode.CatchException 告诉消息循环捕获在处理消息时发生的异常并将其路由到 Application.ThreadException(如果您没有订阅,则 WinForms 会自行处理)。 如果它们没有在那里被捕获,它们可能会解开为未处理的异常,发送 AppDomain.UnhandledException 并卸载 AppDomain。 但是,附加的调试器可以拦截它并恢复原始调用堆栈 (?),并可能允许您在其周围进行调试(如果您可以设置下一个语句以避免它)。 没有调试器,UE 事件返回后您将得到死亡对话框。 - Rob Parker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接