Can't access winforms label after await? - c#

I have a long running method which I made async. I made my button click handler async as well, but when I try to access my label in my button click after the long method is done, it tells me it can't can't access it from another thread. Here is the code:
private void Migrate()
{
for (int i = 2; i <= excelData.GetUpperBound(0); i++)
{
var poco = new ExpandoObject() as IDictionary<string, object>;
foreach (var column in distributionColumnExcelHeaderMappings)
{
if (column.ColumnIndex > 0)
{
var value = excelData[i,column.ColumnIndex]?.ToString();
poco.Add(column.DistributionColumnName.Replace(" ", ""), value);
}
}
pocos.Add(poco);
}
migrationRepository.BulkInsert(insertToTable, "Id", pocos);
}
private async void btnMigrate_Click(object sender, EventArgs e)
{
Task task = new Task(()=> Migrate());
task.Start();
lblStatus.Text = "Migrating data....";
await task;
lblStatus.Text = "Migration Complete";
}
When the button is clicked, I see the status Migrating data..... When that is complete, it throws an error on lblStatus.Text = "Migration Complete". I thought after await, it goes back to the UI thread?
I cleared out most of the code and it still throws the same error. This is a VSTO excel add-in. Could that be part of the problem?
private void Migrate()
{
}
private async void btnMigrate(object sender, EventArgs e)
{
Task.Run(()=>Migrate());
lblStatus.Text = "Done"; //still get error here
}

Try and update your code to the following:
Instead of creating your task and then starting it manually, update it to just await on Task.Run:
private async void btnMigrate_Click(object sender, EventArgs e)
{
lblStatus.Text = "Migrating data....";
await Task.Run(()=> Migrate());
lblStatus.Text = "Migration Complete";
}
Edit:
You can use a helper method that will check to see if the label needs to be invoked before updating.
private async void btnMigrate_Click(object sender, EventArgs e)
{
SetLabelText(lblStatus, "Migrating data....");
await Task.Run(()=> Migrate());
SetLabelText(lblStatus, "Migration complete.");
}
private void SetLabelText(Label label, string text)
{
if (label.InvokeRequired)
{
label.BeginInvoke((MethodInvoker) delegate() {label.Text = text;});
}
else
{
label.Text = text;
}
}

Related

C# Writing to file hangs UI even when using await Task

I'm trying to save a rather large text file when the user hits the save button. It can be up to 30MBs. After pressing the button, I'd like the texbox to display "Saving..." as it's saving the file and when it completes, display "Saved". However I can't get this to work. I've tried using Task.run, await task.Run, and using a background worker. All these options hang the UI until the save completes. The textbox does not display "Saving..." until after it saves and the program is unresponsive until then. How can I fix this?
private async void btnSave_Click(object sender, RoutedEventArgs e)
{
SaveFileDialog saveFileDialog1 = new SaveFileDialog();
saveFileDialog1.ShowDialog();
// If the file name is not an empty string open it for saving.
if (saveFileDialog1.FileName != "")
{
logFileName = saveFileDialog1.FileName;
btnOpenFile.IsEnabled = false;
btnSave.IsEnabled = false;
tbText1.Text += "\n\n***Saving...***\n";
tbText1.ScrollToEnd();
await Task.Run(() => File.WriteAllText(logFileName, Results.ToString()));
tbText1.Text += "\n\n***SAVED***\n\n";
tbText1.ScrollToEnd();
btnOpenFile.IsEnabled = true;
btnSave.IsEnabled = true;
}
As discussed in the comments, the problem is with Results.ToString().
I tried to reproduce the issue with this code:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
for (int i = 1; i < 40536; i++)
{
stringBuilder.Append(new string('a', i));
}
}
readonly StringBuilder stringBuilder = new StringBuilder();
int tickNumber = 0;
private void sync_Click(object sender, EventArgs e)
{
button1.Enabled = false;
stringBuilder.ToString();
button1.Enabled = true;
}
private async void async_Click(object sender, EventArgs e)
{
button2.Enabled = false;
await Task.Run(() => stringBuilder.ToString());
button2.Enabled = true;
}
private void timer1_Tick(object sender, EventArgs e)
{
tickNumber %= 50;
tickNumber++;
label1.Text = new string('.', tickNumber);
}
}
But it works as expected:
Sometimes UI hands for a little bit though. Is this what are you talking about?
Try moving code that generates contents for StringBuilder inside the task (so this StringBuilder only exists in background thread)

Long running task postpones execution of sequentially-previous code after event trigger

I hope I described the problem correctly.
In the following code the drive.IsReady takes some time to complete. Prior to this is the command to print the text "Scanning drives..." in the textbox. The text though appears after the foreach() has completed.
Why is this happening and how can I notify the user prior to the long-running task?
public Form1()
{
InitializeComponent();
button1.Click += new System.EventHandler(this.Button1_Click);
}
private void Button1_Click(object sender, EventArgs e)
{
richTextBox1.Text = "Scanning drives, please wait...";
PopulateComboBox();
}
void PopulateComboBox()
{
System.IO.DriveInfo[] drives = System.IO.DriveInfo.GetDrives();
foreach (System.IO.DriveInfo drive in drives)
{
if (drive.IsReady)
{
comboBox1.Items.Add(drive.Name + drive.VolumeLabel);
}
else
{
comboBox1.Items.Add(drive.Name);
}
}
}
These are the minimal changes required to make the slow part of your code (drive.IsReady) run asynchronously. It won't run faster, the intention is just to keep the UI responsive.
private async void Button1_Click(object sender, EventArgs e) // + async
{
richTextBox1.Text = "Scanning drives, please wait...";
await PopulateComboBox(); // + await
}
async Task PopulateComboBox() // async Task instead of void
{
System.IO.DriveInfo[] drives = System.IO.DriveInfo.GetDrives();
foreach (System.IO.DriveInfo drive in drives)
{
if (await Task.Run(() => drive.IsReady)) // + await Task.Run(() => ...)
{
comboBox1.Items.Add(drive.Name + drive.VolumeLabel);
}
else
{
comboBox1.Items.Add(drive.Name);
}
}
}

c# winforms my ui still freeze with async in event load

In my event load of my form , I call a method loadDg:
private void form_Load(object sender, EventArgs e)
{
loadDg();
}
and
private async Task loadDg()
{
pictureLoading.Visible = true;
await Task.Run(() => { string[] datas = db.row("select * from products");
string[] datas2 = db.row("select * from users");
double one = Convert.ToInt32(datas[0]);
label1.Text = one.toString();
//....
});
pictureLoading.Visible = false; //hide gif animation
}
in my code , db.row This method always returns only 1 row ( string array) , but my ui freezes still , i try update UI continuously with async without freeze at startup
There is nothing to prevent your code run asynchronously. pictureLoading will be invisible even before task is completed. You should fix cross-thread problem and logic of the UI as this:
private void form_Load(object sender, EventArgs e)
{
pictureLoading.Visible = true;
loadDg();
}
private async Task loadDg()
{
await Task.Run(() =>
{
string[] datas = db.row("select * from products");
string[] datas2 = db.row("select * from users");
double one = Convert.ToInt32(datas[0]);
label1.BeginInvoke((Action)delegate ()
{
label1.Text = one.toString();
//hide gif animation
pictureLoading.Visible = false;
});
//....
});
}
Unnecessarily jumping between threads/context should be avoided.
This is an with better resource usage:
private async void form_Load(object sender, EventArgs e)
{
pictureLoading.Visible = true;
try
{
label1.Text = await LoadDgAsync();
}
catch
{
// error handling
}
finally
{
pictureLoading.Visible = false;
}
}
private Task<string> LoadDgAsync()
{
return Task.Run(() =>
{
string[] datas = db.row("select * from products");
string[] datas2 = db.row("select * from users");
double one = Convert.ToInt32(datas[0]);
//....
return one.toString();
});
}
You are calling the loadDg() function synchronously.
Unless you await the loadDg() function call (since its return type is Task) and make the form_Load function asynchronous the function call will be synchronous.
The correct way to fix it is...
private async void form_Load(object sender, EventArgs e)
{
await loadDg();
}

How to change label.text use Task.Run()

No work await Task.Run():
private async void button2_Click(object sender, EventArgs e)
{
await Task.Run(() => {
monitor_r(label1);
});
}
protected async Task monitor_r(Label L1)
{
MessageBox.Show(L1.Name);
L1.ForeColor = Color.Blue;
L1.Text = "test";
}
These commands
MessageBox.Show(L1.Name);
and
L1.ForeColor = Color.Blue;
works fine but
L1.Text = "test";
does not work.
Can you help, why do not change a Label Text?
Try Control.Invoke: we should run Winform UI in the main thread only
protected async Task monitor_r(Label L1)
{
Action action = () =>
{
MessageBox.Show(L1.Name);
L1.ForeColor = Color.Blue;
L1.Text = "test";
};
if (L1.InvokeRequired)
L1.Invoke(action); // When in different thread
else
action(); // When in the main thread
}
If you're on debug mode, take a look at the output window. It should shows exception message something like this:
System.InvalidOperationException' in System.Windows.Forms.dll.
That because label1 accessed from a thread other than the thread it was created on. And it will causing invalid cross-thread operation.
You can solve this by using Control.Invoke as Dmitry Bychenko already mentioned. Here is simple extension to make thread-safe calls to Winforms Control.
public static void TryInvoke(this Control control, Action<Control> action)
{
if (control.InvokeRequired) control.Invoke(new Action(() => action(control)));
else action(control);
}
Sample usage
label1.TryInvoke(x => x.Text = "test");
label1.TryInvoke(x => x.ForeColor = Color.Blue);
Or
this.TryInvoke(x =>
{
label1.Text = "test";
label1.ForeColor = Color.Blue;
});
Secondly, since you don't await anything at monitor_r, i'd recommend to use void instead of async Task.
Even if you're await something at monitor_r you don't need
await Task.Run(() => {
monitor_r(label1);
});
..because monitor_r itself is a task. So just call await monitor_r(label1);
If you wish to have a separate thread, you can try this using BackgroundWorker. You can implement the ReportProgress if you have a loop.
private void button1_Click(object sender, EventArgs e)
{
BackgroundWorker worker = new BackgroundWorker()
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
worker.RunWorkerAsync();
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show(label1.Name);
label1.ForeColor = Color.Blue;
label1.Text = "test";
}

Display the current variable value while looping

private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
string IDs = ID.Text;
string[] eachIDs = Regex.Split(IDs, "\n");
foreach (var eachID in eachIDs)
{
getContent(eachID);
titleBox.Text = "Done";
}
}
private void getContent(string value)
{
label1.Text = value;
Thread.Sleep(5000);
}
I will give 4 id's as Input say "IDNUMBER01, IDNUMBER02, IDNUMBER03, IDNUMBER04" each in a new line in Rich Text Box.
The code splits them successfully. I want to show the Value of the ID being used in the current loop in a Label Text.
Problem with my code is it shows only the last ID which goes through the loop.
Probably your UI freezing and you can't see the changes.Try this, use async/await feature:
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
string IDs = ID.Text;
string[] eachIDs = Regex.Split(IDs, "\n");
foreach (var eachID in eachIDs)
{
await getContent(eachID);
titleBox.Text = "Done";
}
}
private async Task getContent(string value)
{
label1.Text = value;
await Task.Delay(5000);
}
This is because the UI is only Updated after the execution of this code, since they are executing in the same thread. You will need to open a thread, run this code, and call the dispatcher (or the Control.BeginInvoke if this app is Winforms) to update the UI.
EDIT
Try this:
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
string IDs = ID.Text;
new System.Threading.Thread(() =>
{
string[] eachIDs = Regex.Split(IDs, "\n");
foreach (var eachID in eachIDs)
{
getContent(eachID);
titleBox.BeginInvoke((Action) delegate { titleBox.Text = "Done"; });
}
}).Start();
}
private void getContent(string value)
{
label1.BeginInvoke((Action) delegate { label1.Text = value; });
Thread.Sleep(5000);
}
In your example, you'd be better using a timer to display your value text. You're only seeing the last ID because the loop is executing very quickly, and using Thread.Sleep within the foreach isn't going to fly.
You could use Application.DoEvents() before the Thread.Sleep, but a timer is still your better option ... imho.

Categories