I am working on a big WinForms project that controls several Forms on the same UI Thread.
several of this forms has the ability to take and analyze some data from a DB, this is done using an await (for not freezing all the forms while waiting to the data and analyzing it).
I want to make sure i don't have a problem when the UI thread is continuing after an await in a disposed form (if the user closed the form while Task is still running).
I did a search in google and find this:
How to better handle disposed controls when using async/await
in this page the author writes that an Exception is thrown in the situation above (when the UI thread tries to access a label in a disposed form).
I did a test run of this situation and I did not get any Exception thrown:
public partial class Simple_Form : Form
{
public Simple_Form()
{
InitializeComponent();
}
public async Task startCheck(Form1 caller)
{
caller.richTextBox1.Text += "Thread:" + Thread.CurrentThread.ManagedThreadId.ToString() + "|start\n";
label1.Text = "Thread:" + Thread.CurrentThread.ManagedThreadId.ToString() + "|start";
await Task.Delay(10000);
caller.richTextBox1.Text += "Thread:" + Thread.CurrentThread.ManagedThreadId.ToString() + "|stop\n";
caller.richTextBox1.Text += "Thread:" + Thread.CurrentThread.ManagedThreadId.ToString() + "|" + label1.IsDisposed + "\n";
label1.Text = "Thread:" + Thread.CurrentThread.ManagedThreadId.ToString() + "|stop";
}
I tried to run the StartCheck and closing the Simple_Form Form while the UI thread was in the await state.
this code running without any Exceptions thrown although the UI thread tried to change a Disposed label (label1), label1.IsDisposed was "true".
am i missing something or does this functionality changed since the creation of the page above?
Edit:
As requested, the main form i ran:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
Simple_Form newForm;
private async void button2_Click(object sender, EventArgs e)
{
newForm = new Simple_Form();
newForm.Show();
await newForm.startCheck(this);
return;
}
private void button1_Click(object sender, EventArgs e)
{
newForm.Dispose();
}
private void button3_Click(object sender, EventArgs e)
{
richTextBox1.Text += "Thread:" + Thread.CurrentThread.ManagedThreadId.ToString() + "|Still alive.\n";
}
}
I am creating Simple_Form by clicking button2.
I tried disposing it by clicking button1 or by just clickinh the "X" button on the Simple_Form form, both ways worked without any Exceptions thrown.
Edit 2: Changed the code as recommended, The original question still stands.
Funny, that's my linked question. Anyways, the solution is simple. Use this pattern:
await Whatever();
if (IsDisposed)
return;
Why is this necessary? Well the await
call captures the current SynchronizationContext
and then posts back to it.
That means you're back on the original thread. In this case, the GUI thread.
While that's happening asynchronously GUI objects can be disposed of for various reasons (most commonly a form close by the user). Remember, await
is not a blocking call.
So you should protect yourself with IsDisposed
check(s) every single time you await
on the GUI thread.
Specifically, check this flag on any controls modified after the await
call in the same method (that includes Form
which is derived from Control
).
However, you need to understand how Exceptions are handled by Tasks:
If you do an await
you can try ... catch
around it. If you dont use await
exceptions do not bubble up. Here's a simple example.
Task.Run(() => { ... });
This will not raise an exception you can catch unless it is awaited on. If you aren't using await
you can check for the exception using Task.Exception
like so:
var task = Task.Run(() => { ... });
//...SNIP...
if (task.Exception != null)
//Do something
Other issues with your code:
public async void StartCheck(Form1 caller)
should be
public async Task StartCheck(Form1 caller)
The only time an async method should not return Task
or Task<T>
is if you're not allowed to use that signature (like button click handlers).
Finally, use Task.Delay
not Thread.Sleep
. Change
await Task.Run(() =>
{
Thread.Sleep(10000);
});
To
await Task.Delay(10000);
Edit
Try this:
public async Task startCheck(Form1 caller)
{
await Task.Delay(10000);
this.Show();
}
After you have closed newForm
but before the await
has completed. An exception will be thrown.
This should also cause the behavior you expect:
newForm.Dispose(true);