ObjectDisposedException on call to Form's Invo

2019-04-10 19:36发布

We get an ObjectDisposedException from a call to Invoke on a Form that hasn't yet been disposed. Here's some sample code demonstrating the problem:

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")));
    }
}

Here's the calling code from Form1 (the main form) which I never close:

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(); 
    }
}

We dug in to the Microsoft source, and found the exception is created during the call to DestroyHandle (below). DestroyHandle is being called by ShowDialog as it is finishing.

From 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();
                    }
                }
            } 
        }
    // ...
}    

Questions:

  1. Why is ShowDialog destroying the handle (when I might re-use this form)?

  2. Why am I getting an ObjectDisposedException--its very misleading (since its not disposed). Its as if the code expected the handle would be destroyed only when the object was disposed (which is what I was expecting).

  3. Should this be valid? That is, should I be allowed to Invoke on to a control after ShowDialog?

Notes:

  1. Doing a second .ShowDialog() causes a new handle to be created.

  2. If after doing .ShowDialog() I try to do an Invoke, I get an InvalidOperationException stating that "Invoke or BeginInvoke cannot be called on a control until the window handle has been created."

  3. If after doing .ShowDialog() I access the Handle property, and then do an Invoke, it will succeed.

1条回答
神经病院院长
2楼-- · 2019-04-10 20:30

Why is ShowDialog destroying the handle (when I might re-use this form)?

To destroy the native window and make it disappear. The Handle property will be IntPtr.Zero after this.

Why am I getting an ObjectDisposedException--its very misleading (since its not disposed).

Yes, it is misleading in case of a dialog. The code was written to deal with the more common case of a form that's displayed with Show(). Once the native window is destroyed, it works down the queue of pending invokes and marks them complete. And sets their "last exception raised" status to ObjectDisposedException (entry.exception in your Reference Source snippet). It does this explicitly to prevent any invoked code from running with the native window gone, such code will very commonly die with an ODE. It just jumps the gun and raises the exception early. You could argue that an InvalidOperationException would be more appropriate, but they chose ODE.

Should this be valid? That is, should I be allowed to Invoke on to a control after ShowDialog?

No, that cannot work. The value of the Handle property is required to figure out what thread to invoke to. But it is IntPtr.Zero after ShowDialog() returns.

You hit a corner case here, but the general strategy must always be that you ensure that threads are completed or terminated before you allow the form to close. More about that in this answer.

查看更多
登录 后发表回答