简体   繁体   中英

IProgress<T> results in NullReferenceException which can't be caught

I'm toying around with C# and trying to practice some WinForms with async code. I have the following code, but when I click the button and initiate doTheThingAsync and I close the window while the task is still processing then Visual Studio catches a NullReferenceException from the point where I access toolStripProgressBar1.Value .

private async Task doTheThingAsync(IProgress<int> progress)
{
    await Task.Run(() =>
    {
        for (int i = 1; i <= 1000; ++i)
        {
            Thread.Sleep(10);
            progress.Report(i);
        }
        Thread.Sleep(2000);
    });
}

private async void button1_Click(object sender, EventArgs e)
{
    var progress = new Progress<int>(i => toolStripProgressBar1.Value = i);
    await doTheThingAsync(progress);
    toolStripProgressBar1.Value = 0;
}

Not sure why this is happening, but I figure if I cancel the task before the Form closes then all will be well, so I modify it.

private async Task doTheThingAsync(IProgress<int> progress, CancellationToken ct)
{
    await Task.Run(() =>
    {
        for (int i = 1; i <= 1000; ++i)
        {
            if (ct.IsCancellationRequested) return;
            Thread.Sleep(10);
            progress.Report(i);
        }
        Thread.Sleep(2000);
    });
}

private async void button1_Click(object sender, EventArgs e)
{
    var progress = new Progress<int>(i => toolStripProgressBar1.Value = i);
    var cts = new CancellationTokenSource();
    FormClosed += (s, ev) => cts.Cancel();
    await doTheThingAsync(progress, cts.Token);
    toolStripProgressBar1.Value = 0;
}

No dice. I still get the exception. So out of desperation I try wrapping it in a try/catch.

var progress = new Progress<int>(i => {
    try {
        toolStripProgressBar1.Value = i;
    } catch {}
});

But even then the uncaught NullReferenceException persists, and I am out of ideas.

System.NullReferenceException
    HResult=0x80004003
    Message=Object reference not set to an instance of an object.
    Source=System.Windows.Forms
    StackTrace:
    at System.Windows.Forms.ToolStripProgressBar.set_Value(Int32 value)
    at Test.Form1.<>c__DisplayClass2_0.<button1_Click>b__0(Int32 i) in C:\Users\Chris\source\repos\Test\Test\Form1.cs:line 37

Edit: The NullReferenceException seems to come from the fact that ToolStripProgressBar.Value has a { get, set } that accesses a reference that is nulled due to the fact that the control has already been disposed at the time of access.

Fildor's request:

private Task doTheThingAsync(IProgress<int> progress, CancellationToken ct)
{
    return Task.Run(() =>
    {
        for (int i = 1; i <= 1000; ++i)
        {
            ct.ThrowIfCancellationRequested(); //VS breaks here with unhandled exception
            Thread.Sleep(10);
            progress.Report(i);
        }
        Thread.Sleep(2000);
    });
}

private async void button1_Click(object sender, EventArgs e)
{
    var progress = new Progress<int>(i => {
        toolStripProgressBar1.Value = i;
    });
    var cts = new CancellationTokenSource();
    FormClosed += (s, ev) => cts.Cancel();
    try
    {
        await doTheThingAsync(progress, cts.Token);
        toolStripProgressBar1.Value = 0;
    }
    catch (OperationCanceledException) { }
}

Edit: The issues with the above code is that is should have been TaskCanceledException not OperationCanceledException .

Update: This is the only way I have been able to get this to work such that I can close the form without getting an unhandled exception. Note that I am having to always check IsDisposed before accesing Value on the ToolStripProgressBar from within the IProgress action. I can't say for sure why but this seems absurd to me.

    private Task doTheThingAsync(IProgress<int> progress, CancellationToken ct)
    {
        return Task.Run(async () =>
        {
            for (int i = 1; i <= 1000; ++i)
            {
                ct.ThrowIfCancellationRequested();
                await Task.Delay(10, ct);
                progress.Report(i);
            }
            await Task.Delay(2000, ct);
        });
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        var progress = new Progress<int>(i => {
            if (!toolStripProgressBar1.IsDisposed) toolStripProgressBar1.Value = i;
        });
        var cts = new CancellationTokenSource();
        //FormClosed += (s, ev) => cts.Cancel(); //no longer actually necessary...
        try
        {
            await doTheThingAsync(progress, cts.Token);
            toolStripProgressBar1.Value = 0;
        }
        catch {}
    }

Edit Changed from label to report result to a ToolStripProgressBar which as stated from others needs to check _cts.IsCancellationRequested

Here is a code sample to try out which will not throw an exception on closing the form/app while a do nothing async method is running.

Starts off in a button named StartButton, first checks if the CancellationTokenSource needs to be recreated if used already, performs a do nothing method using await Task.Delay(500, ct) rather than Thread.Sleep which has already been pointed out to avoid.

In the do nothing method AsyncMethod we test if there is a cancel request which goes back to the start button code which handles the request in a try/catch.

Cancellation is done in a button named CancelButton.

Progress is done via setting value property of a ToolStripProgressBar .

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace AsyncSimple
{
    public partial class Form1 : Form
    {
        private CancellationTokenSource _cts = new ();
        public Form1()
        {
            InitializeComponent();
        }

        private async void StartButton_Click(object sender, EventArgs e)
        {
            if (_cts.IsCancellationRequested)
            {
                _cts.Dispose();
                _cts = new CancellationTokenSource();
            }

            var progressIndicator = new Progress<int>(ReportProgress);

            try
            {
                await AsyncMethod(progressIndicator, _cts.Token);
            }
            catch (OperationCanceledException)
            {
                // do nothing
            }
        }

        private void CancelButton_Click(object sender, EventArgs egEventArgs)
        {
            _cts.Cancel();
        }
        private static async Task AsyncMethod(IProgress<int> progress, CancellationToken ct)
        {

            for (int index = 1; index <= 100; index++)
            {
                //Simulate an async call that takes some time to complete
                await Task.Delay(500, ct);

                progress?.Report(index);

            }

        }

        private void ReportProgress(int value)
        {
            if (_cts.IsCancellationRequested)
            {
                return;
            }
            else
            {
                toolStripProgressBar1.Value = value;
            }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM