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:
Why is ShowDialog destroying the handle (when I might re-use this form)?
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).
Should this be valid? That is, should I be allowed to Invoke on to a control after ShowDialog?
Notes:
Doing a second
.ShowDialog()
causes a new handle to be created.If after doing
.ShowDialog()
I try to do anInvoke
, I get an InvalidOperationException stating that "Invoke or BeginInvoke cannot be called on a control until the window handle has been created."If after doing
.ShowDialog()
I access theHandle
property, and then do anInvoke
, it will succeed.
To destroy the native window and make it disappear. The Handle property will be IntPtr.Zero after this.
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.
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.