Assumptions
Knowledge of .NET Windows Forms event handling and threading is assumed, including the use of the lock
statement, and how to invoke events onto a GUI thread.
Overview
This article explains how to properly close a multi-threaded .NET Windows Forms application where there is a thread running in the background that fires events which update or modify the GUI. The main problem that can arise if such an application is not closed down properly (via code) is that an ObjectDisposedException
may get thrown with the message, "Cannot access a disposed object". This error occurs because the GUI gets disposed of first before the background thread has a chance to close, causing an event to get fired which tries to update the disposed object. The key things leading to the problem are the GUI getting disposed of first before the background thread, and the background thread firing events that update the GUI. Note that if it so happens that the background thread closes first before the GUI, then this problem doesn’t arise, which is why you may have noticed that this problem doesn’t occur consistently every time.
The following is a diagram of what the problem looks like:
Problem
The following code shows how to reproduce the problem.
void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
this.CloseBackgroundWorker = true;
}
void BackgroundWorkerThread()
{
while (this.CloseBackgroundWorker != true)
{
SomeEvent(null, null);
Thread.Sleep(1);
}
}
void Form1_SomeEvent(object sender, EventArgs e)
{
if (this.InvokeRequired == true)
{
EventHandler eh = new EventHandler(Form1_SomeEvent);
this.Invoke(eh, new object[] { null, null });
}
else
{
label1.Text = "Counter value: " + _counter++;
}
}
Basically, when the user tells the application to close, the thread running in the background, BackgroundWorkerThread
, gets signaled to close, and the GUI, Form1
, gets disposed of. However, and here’s the problem, there’s no guarantee that BackgroundWorkerThread
will close first before Form1
is disposed of. This is a big problem, because if BackgroundWorkerThread
runs for even a short amount of time after Form1
has been disposed of, there’s a good chance that the event that updates the GUI, SomeEvent
, will get fired (see label1.Text
getting changed in the Form1_SomeEvent
event handler). And, because Form1
at this point has already been disposed of, it’s not possible to update it, and an ObjectDisposedException
exception will get thrown.
To reproduce this behavior, simply download, build, and run the application, then click the exit button to close the application. The exception may not get thrown every time, as noted earlier, so simply run the application, and try again if nothing happens. Note, the while
loop in BackgroundWorkerThread
has a very small timeout in order to increase the odds for the exception to get thrown.
Solution
The solution to this problem is pretty simple in concept. When the user attempts to close the GUI, the GUI should first wait until the thread running in the background closes, and then, only after that has happened, proceed in shutting itself down. So, in the example code shown above, Form1
should first wait until BackgroundWorkerThread
has closed, and only when that has happened, proceed in closing itself down.
The following is a diagram of the solution:
Here is the code for the solution:
bool _safeToCloseGUI = false;
void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
this.CloseBackgroundWorker = true;
if (_safeToCloseGUI == false)
{
e.Cancel = true;
}
}
void BackgroundWorkerThread()
{
while (this.CloseBackgroundWorker != true)
{
SomeEvent(null, null);
Thread.Sleep(1);
}
CloseGUI(null, null);
}
void Form1_SomeEvent(object sender, EventArgs e)
{
if (this.InvokeRequired == true)
{
EventHandler eh = new EventHandler(Form1_SomeEvent);
this.Invoke(eh, new object[] { null, null });
}
else
{
label1.Text = "Counter value: " + _counter++;
}
}
void Form1_CloseGUI(object sender, EventArgs e)
{
if (this.InvokeRequired == true)
{
EventHandler eh = new EventHandler(CloseGUI);
this.Invoke(eh, new object[] { null, null });
}
else
{
_safeToCloseGUI = true;
this.Close();
}
}
A few changes have been made to the original code.
First, a new flag has been added to the Form1_FormClosing
method, _safeToCloseGUI
, that allows the GUI to be closed only when the flag is set to true
. When the user clicks to close the application, the background worker thread will be signaled to close just like before. However, now the GUI’s close operation will be canceled out because the _safeToCloseGUI
flag is set to false
initially. As will be seen next, this flag will be set to true
once it is safe to close down the GUI.
Next, BackgroundWorkerThread
has been modified so that once it is signaled to close down, it will break out of its while
loop, and then afterwards fire an event that tells the GUI to close down. After BackgroundWorkerThread
has broken out of its while
loop, it is guaranteed to not fire the event that updates the GUI, and therefore at this point, it is safe for the GUI to shutdown. Therefore, BackgroundWorkderThread
will fire the CloseGUI
event to tell the GUI to shut down.
Finally, the event handler Form1_CloseGUI
has been added to handle the CloseGUI
event. First, the handler sets the _safeToCloseGUI
flag to true
, and then it calls the Close
method. This will cause Form1_FormClosing
to continue because _safeToCloseGUI
is now true
, and the GUI will safely and successfully shut down.
Alternate Approach
The following is another way to solve this issue with a lot less code, albeit the solution is not as elegant. Basically, a try
/catch
can be placed around the invoke line where the GUI is updated so that if an exception is thrown, it will get handled and the application will not crash. See the following code:
void Form1_SomeEvent(object sender, EventArgs e)
{
if (this.InvokeRequired == true)
{
EventHandler eh = new EventHandler(Form1_SomeEvent);
try
{
this.Invoke(eh, new object[] { null, null });
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
else
{
label1.Text = "Counter value: " + _counter++;
}
}
This approach is not as elegant because an exception still occurs upon exit of the application (even though it is handled and there’s no crash). However, if you want to avoid the exception altogether, the recommended approach is the originally proposed solution. The original solution might be a little bit more work, but I would argue that it is really the “cleaner” solution.