How do I avoid the dreaded Application.DoEvents() when multithreading [closed]
-
21-12-2019 - |
Question
So I've been reading a lot of articles that have been discouraging the use of Application.DoEvents() and even saying that it should never be used but I can't seem to find a good alternative for my scenario... The application that I am working on has a method that is called by a this.Shown event when the main GUI form first launches. The method does some work that takes about a minute of time so the same method also creates a form that is essentially a custom made progress bar. Keep in mind that this process is currently single threaded so when this method is doing work the main GUI and and progress bar become non-responsive. If the user clicks anywhere during this time, the screens go blank. So I'm working on putting some of the work that this method does in a BackgroundWorker thread. Here is what I've come up with:
private BackgroundWorker Bgw = new BackgroundWorker();
private int LoadPercentage = 0;
//this sub is executed on the main UI thread
public void RunBgw()
{
bool StopThread = false;
//this object should be created in this method and needs to be updated as the child thread is doing work
MyCustomDialog dialog = new MyCustomDialog();
dialog.UpdateProgress(0, "My message");
dialog.Show();
this.Invalidate();
this.Refresh();
//critical properties to set if you want to report progress/be able to cancel the operation
Bgw.WorkerSupportsCancellation = true;
Bgw.WorkerReportsProgress = true;
//add handlers to Bgw so events will fire
Bgw.DoWork += new DoWorkEventHandler(Bgw_DoWork);
Bgw.ProgressChanged += new ProgressChangedEventHandler(Bgw_ProgressChanged);
Bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Bgw_RunWorkerCompleted);
//fire off thread
Bgw.RunWorkerAsync();
while (Bgw.IsBusy == true)
{
if (BW.CancellationPending == true)
{
StopThread = true;
break;
}
Application.DoEvents();
if(LoadPercentage == 10)
{
dialog.UpdateProgress(LoadPercentage, "Still working...");
this.Invalidate();
this.Refresh();
}
if(LoadPercentage == 50)
{
dialog.UpdateProgress(LoadPercentage, "Halfway done...");
this.Invalidate();
this.Refresh();
}
// etc...
//slow down loop so it doesnt take up all the CPU
Thread.Sleep(200);
}
if(!StopThread) {
//continue with something else.
}
}
private void Bgw_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker BgwLocal = sender as BackgroundWorker;
if ((BgwLocal.CancellationPending == true))
{
e.Cancel = true;
break;
}
else
{
TimeConsumingWork();
BgwLocal.ReportProgress(10); //report progress back to the main UI thread
TimeConsumingWork();
BgwLocal.ReportProgress(15, SomeGuiIcon); //pass back specific gui icon
TimeConsumingWork();
BgwLocal.ReportProgress(50);
// etc...
}
}
private void Bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
LoadPercentage = e.ProgressPercentage; //set current percentage of progress
if(LoadPercentage == 15)
{
GuiIcon1 = (Icon)e.UserState; //set gui icon
}
}
private void Bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if ((e.Cancelled == true))
{
//error handling
}
else if (!(e.Error == null))
{
//error handling
}
else
{
//success
}
}
Everything is working well except that error handling has proven to be difficult and messy. Is there a better way of threading work while updating an existing object in the main thread??
Thanks for reading.
Solution
You should not be blocking the UI thread with this code:
while (Bgw.IsBusy == true) { ... }
Instead, allow RunBgw()
to return to the caller. Use the events present on the BackgroundWorker to know when it has completed. Specifically
Bgw.ProgressChanged += new ProgressChangedEventHandler(Bgw_ProgressChanged);
reports on progress by calling
Bgw_ProgressChanged
and
Bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Bgw_RunWorkerCompleted);
causes
Bgw_RunWorkerCompleted
to be invoked when the BackgroundWorker is done.
Update the progress bar from within Bgw_ProgressChanged
.
Windows UI's are event driven. Your code is not making use of events to control program execution.