Search code examples
c#multithreadingwinformsdispose

control already disposed when returning from await


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.


Solution

  • 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);