RaineWingel
RaineWingel

Reputation: 25

VB.NET - filling DataGridView using Background worker without freezing the form

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

Answers (1)

Harald Coppoolse
Harald Coppoolse

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 thread
  • BackGroundWorker.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:

  • Do all lengthy processing in DoWork
  • ProgressChanged and RunWorkerCompleted are executed by the UI thread. They should be lightWeight
  • BackgroundWorker should not use any value from your form; communicate using the parameters in the events.
  • Consider to support cancellation. If your form is closing while the worker thread is still working, you can cancel and wait until the background worker is completed.

Upvotes: 1

Related Questions