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

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();
}

Related

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

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";
}

Can't access winforms label after await?

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

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.

Making sure a backgroundworker has finished before running it (again)

Context: I am playing music through a media element, and using a slider to display the point in the song that it is at. That updating is done in a backgroundworker, for obvious reasons.
private void bgPlay_DoWork(object sender,DoWorkEventArgs e)
{
while (isMediaPlaying)
{
this.Dispatcher.Invoke((Action)(() =>
{
timelineSlider.Value = mediaElement1.Position.TotalMilliseconds;
}));
}
}
private void Library_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
try
{
mediaElement1.Stop();
isMediaPlaying = false;
mediaElement1.Source = new Uri(songData[Library.SelectedIndex].Location);
mediaElement1.Volume = (double)volumeSlider.Value;
mediaElement1.Play();
isMediaPlaying = true;
bgPlay.RunWorkerAsync();
}
catch(Exception ex) {
F.MessageBox.Show(ex.ToString());
}
}
When I play a song, then double click on a different one, the background worker is still looping and throws an exception because it reaches bgPlay.RunWorkerAsync(); before the previous instance has finished. I tried to use the isMediaPlaying bool to tell the backgroundworker when to exit the loop, but the main thread reaches bgPlay.RunWorkerAsync(); before it finishes.
You are suffering of a common mistake when one is barely starting to program with threading, a race condition
I'd advise rewriting the code like this:
private static String threadingLock = "";
private void bgPlay_DoWork(object sender,DoWorkEventArgs e)
{
while (true)
{
lock(threadingLock) {
if(!isMediaPlaying)
break;
}
this.Dispatcher.Invoke((Action)(() =>
{
timelineSlider.Value = mediaElement1.Position.TotalMilliseconds;
}));
}
}
private void Library_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
try
{
lock(threadingLock) {
isMediaPlaying = false;
}
mediaElement1.Stop();
mediaElement1.Source = new Uri(songData[Library.SelectedIndex].Location);
mediaElement1.Volume = (double)volumeSlider.Value;
mediaElement1.Play();
isMediaPlaying = true;
bgPlay.RunWorkerAsync();
}
catch(Exception ex) {
F.MessageBox.Show(ex.ToString());
}
}
As a friendly tip, add a Thread.sleep(200) before invoking the update on the slider. It will reduce cpu usage without affecting the functionality of your application.

Categories