Reputation: 25
My program contains a section where the user is able to fill a DataGridView with contents from a database by clicking a button. I do this by using a Background worker and all in all, my code seems to work fine but I'm not sure if I am implementing the Background worker the way it is meant to be. The problem is that despite using the RunWorkerAsync() method, my entire form still freezes for the 10 seconds or so which it takes to process the data and add new rows to the DataGridView. What I would like to achieve is a solution where the form remains functional and the progress is displayed within a progress bar.
Here is some (pseudo) code of what I'm doing so far:
private myList as new List(of Object)
Private Sub btnClick(sender As Object, e As EventArgs) Handles myButton.Click
myBgWorker.RunWorkerAsync()
End Sub
Private Sub doWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles myBgWorker.DoWork
execute some SQL()
for each SQL-result row
myList.add(rowData)
myBgWorker.ReportProgress()
Loop
End Sub
Private Sub reportProgress(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles myBgWorker.ProgressChanged
pgrogressBar.Value = e.ProgressPercentage
End Sub
Private Sub done(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles myBgWorker.RunWorkerCompleted
For Each entry in myList
Dim rowIdx As Integer = myDGV.Rows.Add()
Dim dgvRow As DataGridViewRow = myDGV.Rows(rowIdx)
dgvRow.Cells(0).Value = "something"
dgvRow.Cells(1).Value = "something else"
dgvRow.Cells(2).Value = "even more..."
Next
MsgBox("I'm done.")
End Sub
So basically the worker executes some SQL, loops through the received rows and fills a list while at the same time representing the progress in a progress bar. So far so good. But as soon as it executes the RunWorkerCompleted code, the form freezes and I have to wait until all the rows have been added to the DataGridView.
I also tried to add each row separately within the reportProgress method but the result is the same and the way I understand these Background workers, you are not meant to update your UI at that point.
Upvotes: 1
Views: 1257
Reputation: 30464
You are right, you are not using the BackgroundWorker correctly. You should be aware which of the events are called on what thread.
You have two threads: your UI thread, that does all the updating, and your BackgroundWorker, that is supposed to do all the lengthy processing.
Standard usage of the BackgroundWorker
(1) UI thread creates an object of class BackGroundWorker, and sets some properties. The most important ones are: WorkerReportsProgress and WorkerSupportsCancellation.
(2) The UI class adds three event handlers to the BackgroundWorker:
BackGroundWorker.DoWork
attach the method that will do the background work. This work will be done by the BackgroundWorker threadBackGroundWorker.ProgressChanged
. Attach the method to update UI to inform operator about progress. This method will be executed by the UI thread.BackGroundWorker.RunWorkerCompleted
. Attach the method that must be executed when the BackgroundWorker is finished, to when the DoWork ends. This method will also be executed by the UI thread.(3) After a while, the UI thread decides that the BackgroundWorker should start working:
Sorry, until now I could avoid using code. My VB is a bit rusty, so you'll have to cope with C#. I'm sure you'll get the gist:
this.backgroundWorker.RunWorkerAsync();
of if you need to pass a parameter:
MyParam myParam = new MyParam() {...}
this.backgroundWorker.RunWorkerAsync(myParam);
A new thread will be created, and it will start executing the methods that are attached to event BackGroundWorker.DoWork
Sub doWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs)
Handles myBgWorker.DoWork
{
BackgroundWorker backgroundWorker = sender as BackgroundWorkder
do some work that doesn't take too long, because you want to report progress
while work not finished and not backGroundWorker.CancellationRequested
{
create a progress report, put it in an object
if possible: calculate an estimation about your progress in %;
// report progress, don't use variable backgroundWorkder, use the sender
(sender as BackgroundWorker).ReportProgress(sender, progressPercentage, progressReport);
}
// if here: finished with your calculations, create a result report:
MyResultReport myReport = ...
e.Result = myReport;
// if you promised to support cancellation or errors:
e.Error = ...
e.Cancelled = backgroundWorker.CancellationRequested;
e.State = ...
End Sub
If you want to report progress, you'll have to put some data in the progress, otherwise the receiver of your progress doesn't have any clue about your progress.
You can use the percentage for a ProgressBar, or something similar. The progressReport can be used to show other information that your UI thread wants to know.
By the way: it is smart if your backgroundWorker doesn't use any variable outside its procedure. Use the Sender to get the BackGroundWorker. Pass all values that the backgroundWorker needs as input parameter. This way, your backgroundWorker can work completely without any knowledge of who started it. This makes it easier to reuse it, easier to change the owner of the worker thread and thus easier to unit test it.
Consider to force this separation by creating a separate class that will do the background work. This is hardly more work. No need to do this if reusability, unit testing, maintainability are not important for you.
When the backgroundWorker is finished doing the heavy calculations, you can put the result of the calculations in e.Result.
Every time the BackgroundWorker reports progress, the UI thread receives this in the event handler that you attached to ProgressChanged
void ProgressReported(object sender, ProgressChangedEventArgs e)
{
// the designer of the worker thread promised to put his progress in a class MyProgressReport:
int progressPercentage = e.progressPercentage;
MyProgressReport progressReport = e.UserState as MyProgressReport;
this.ProgressReported(progressPercentage, progressReport);
}
By the way: if you plan to use this event handler for several BackgroundWorkers you should check sender
, to determine who reports progress and what to do with the progress report.
void ProgressReported(int progressPercentage, MyProgressReport progressReport)
{
this.ProgressBar1.Value = progressPercentage;
this.ProgressTextBox.Text = progressReport.ToString();
}
Finally, if your worker thread has finished the heavy processing, the eventhandlers of RunworkerCompleted are called. The UI thread will handle it:
You should tell the designer of the backgroundWorker what he should put in the results. It will probably all data fetched from the database.
public RunWorkerReportsCompletion(object sender, RunWorkderCompletedEventArgs e)
{
MyResultReport result = e.Result as MyResultReport;
ProcessResult(result);
}
If you expect problems or cancellation, consider to check properties Cancelled
and Error
.
So summarized:
DoWork
Upvotes: 1