当窗体未被释放时调用窗体的Invoke方法会引发ObjectDisposedException异常。

4
我们在调用尚未被处理的窗体上的Invoke时,会收到ObjectDisposedException异常。以下是演示该问题的示例代码:
public partial class Form2 : Form
{
    void Form2_Load(object sender, EventArgs e)
    {
        // Start a task that does an Invoke on this control
        Task.Factory.StartNew(TaskWork); 

        // Sleep here long enough to allow the task that does the Invoke 
        // to execute to the point where it has:
        // a. Posted the message and 
        // b. is waiting 
        Thread.Sleep(500);

        // Cause ShowDialog to return by setting the DialogResult
        DialogResult = DialogResult.OK;
    }

    void TaskWork()
    {
        // This call doesn't return, but instead throws an ObjectDisposedException
        this.Invoke((MethodInvoker)(() => MessageBox.Show("Invoke succeeded")));
    }
}

以下是来自主窗体Form1的调用代码,我从未关闭它:
public partial class Form1 : Form
{
    Form2 m_form2 = new Form2();

    void Form1_Load(object sender, EventArgs e)
    {
        // Call ShowDialog, but don't dispose it.
        m_form2.ShowDialog();

        // Cause the finalizers to run.  This causes an AggregateException to be thrown
        // due to the unhandled ObjectDisposedException from the Task.
        GC.Collect(); 
    }
}

我们深入研究了Microsoft的源代码,并发现异常是在调用DestroyHandle时创建的(如下所示)。ShowDialog在完成时将调用DestroyHandle。
来自source.NET\4\DEVDIV_TFS\Dev10\Releases\RTMRel\ndp\fx\src\WinForms\Managed\System\WinForms\Control.cs\1305376\Control.cs:
protected virtual void DestroyHandle() {
    // ...
        // If we're not recreating the handle, then any items in the thread callback list will
        // be orphaned.  An orphaned item is bad, because it will cause the thread to never 
        // wake up.  So, we put exceptions into all these items and wake up all threads. 
        // If we are recreating the handle, then we're fine because recreation will re-post
        // the thread callback message to the new handle for us. 
        //
        if (!RecreatingHandle) {
            if (threadCallbackList != null) {
                lock (threadCallbackList) { 
                    Exception ex = new System.ObjectDisposedException(GetType().Name);

                    while (threadCallbackList.Count > 0) { 
                        ThreadMethodEntry entry = (ThreadMethodEntry)threadCallbackList.Dequeue();
                        entry.exception = ex; 
                        entry.Complete();
                    }
                }
            } 
        }
    // ...
}    

问题:

  1. 为什么ShowDialog会销毁句柄(当我可能会重用这个窗体)?

  2. 为什么我会收到ObjectDisposedException,这非常具有误导性(因为它没有被处理)。 就像代码期望只有在对象被处理时才会销毁句柄一样(这也是我所期望的)。

  3. 这是否有效? 也就是说,我能在ShowDialog之后调用Invoke到控件上吗?

备注:

  1. 第二个.ShowDialog()会创建一个新的句柄。

  2. 如果在做完.ShowDialog()之后尝试进行Invoke,则会收到InvalidOperationException错误,指出“Invoke或BeginInvoke不能在控件上调用,直到窗口句柄已被创建。”。

  3. 如果在完成.ShowDialog()后访问Handle属性,然后进行Invoke,它将成功。

1个回答

4

为什么ShowDialog会销毁句柄(当我可能要重复使用这个窗体)?

为了销毁本地窗口并使其消失。此后,Handle属性将为IntPtr.Zero。

为什么我会收到ObjectDisposedException - 它非常具有误导性(因为它没有被处理)。

是的,在对话框的情况下,这是具有误导性的。代码是针对更常见的使用Show()显示的窗体编写的。一旦本地窗口被销毁,它就会处理挂起调用队列并标记它们为完成状态。并将它们的“最后一个异常”状态设置为ObjectDisposedException(在您的参考源代码片段中的entry.exception)。它明确执行此操作以防止任何调用的代码在本地窗口消失时运行,这样的代码通常会因ODE而死亡。它只是抢先引发异常。你可以认为InvalidOperationException更合适,但他们选择了ODE。

这应该有效吗?也就是说,我可以在ShowDialog之后调用控件吗?

不,那是不可能的。需要Handle属性的值才能确定要调用的线程。但在ShowDialog()返回后,它为IntPtr.Zero。

你在这里遇到了一个特殊情况,但是一般的策略必须是确保线程在允许表单关闭之前已经完成或终止。更多信息请参见此答案

谢谢您的回答和链接,非常有帮助。我不理解的部分是为什么需要销毁句柄才能隐藏窗口。如果我要连续进行几个ShowDialog操作,销毁句柄似乎是不必要的。我是否遗漏了什么?我一直以为只有在处理时才会销毁句柄。 - Matt Smith
1
我只是这样做,你无能为力。隐藏对话框也会销毁本地窗口。 - Hans Passant
在研究类似问题(关闭窗体时调用invoke)并尝试检查窗体及其控件的IsDisposed属性后,仍然遇到InvalidOperationException异常。我发现即使IsDisposed为false,但在这种情况下Disposing属性也为true。 - NiKiZe

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