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 {}
}