Search code examples
vb.netmultithreadingprogress-barbackgroundworker

How to use a "dummy" (updated via elapsed time) progress bar while using backgroundworker


Working, updated code at the bottom (edit). Thank you, roryap


My program is fairly simple.

You click a button and it performs a task. The task uses an XML exporter for a large table of data. However, it takes a long time due to database distance. It is not iterative so I can't do multiple ProgressChanged calls and I want users to be able to use the rest of the interface during this time.

This poses a problem. Depending on the order being processed, it could have multiple lines per order. Each line takes roughly a minute. What I want is a progress bar (and elapsed timer) to update while the backgroundworker is running.

This is an example of what I'm currently doing:

Private Sub GenerateXMLFiles()
    'Perform a single large, slow task here:
    ExportToXML(dgvDataGrid)
    dPercentComplete = ((dgvDataGrid.Rows.Count * 60) - swTimer.Elapsed.TotalSeconds) / 100
    UpdateStatus(dPercentComplete, MillisecondsToHMS(swTimer.Elapsed.TotalMilliseconds))
End Sub

Private Sub btnGenerateXMLFiles_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGenerateXMLFiles.Click
    barProgress.Value = 0
    barProgress.Minimum = 0
    barProgress.Maximum = 100
    System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = False
    bgWorker.RunWorkerAsync()
    bgWorker.WorkerReportsProgress = True
End Sub

Private Sub bgWorker_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs)
    btnGenerateXMLFiles.Enabled = False
    swTimer.Start()
    GenerateXMLFiles()
End Sub

Private Delegate Sub UpdateStatusDelegate()
    Friend Sub UpdateStatus()
        If InvokeRequired Then
            Invoke(New UpdateStatusDelegate(AddressOf UpdateStatus), New Object() {})
        Else
            dProgress = ((dgvDataGrid.Rows.Count * 60) - swTimer.Elapsed.TotalSeconds) / 100
            barProgress.Value = CInt(dProgress)
            lblElapsedTimeFill.Text = MillisecondsToHMS(swTimer.Elapsed.TotalMilliseconds)
        End If
End Sub

Private Sub bgWorker_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
    If Not (e.Error Is Nothing) Then
        MsgBox(e.Error.Message)
    End If

    barProgress.Value = 100
    btnGenerateXMLFiles.Enabled = True
    swTimer.Stop()

    swTimer.Reset()
    EnableAllButtons()
    bgWorker.Dispose()
End Sub

Private Function MillisecondsToHMS(ByVal ms As Double) As String
    Dim ts As TimeSpan
    Dim totHrs As Integer
    Dim H, M, S, HMS As String

    ts = TimeSpan.FromMilliseconds(ms)
    totHrs = Math.Truncate(ts.TotalHours)
    H = Format(totHrs, "0#") & ":"
    M = Format(ts.Minutes, "0#") & ":"
    S = Format(ts.Seconds, "0#")
    HMS = H & M & S

    Return HMS
End Function

This clearly doesn't work as it won't update the Progress Bar and Elapsed Time but a single time before or after doing the large call.

So the goal is to just have the progress bar take ~60 seconds to fill per order I pass it, since it's quite close to that anyway.

This leads me to believe that I may have to have a separate thread handle the progress bar and timer, but I'm not sure what problems that poses or if it will update regardless. Maybe I'm over thinking this?

Thanks for any help or advice you all can offer!


Update: working!

Private Sub ThreadGenerateXMLFiles()
    'Significant time-consuming XML export function
End Sub

Private Sub btnGenerateXMLFiles_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGenerateXMLFiles.Click
    barProgress.Value = 0
    tTimer.Start()
    Control.CheckForIllegalCrossThreadCalls = False 'This is not smart, but time is limited
    bgWorker.RunWorkerAsync()
    bgWorker.WorkerReportsProgress = True
End Sub

Private Sub bgWorker_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgWorker.DoWork
    btnGenerateXMLFiles.Enabled = False
    swTimer.Start()
    ThreadGenerateXMLFiles()
End Sub

Private Sub bgWorker_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles bgWorker.RunWorkerCompleted
    If Not (e.Error Is Nothing) Then
        MsgBox(e.Error.Message)
    End If

    btnGenerateXMLFiles.Enabled = True
    DisableGeneratingXMLOverlay()
    swTimer.Stop()
    swTimer.Reset()
    tTimer.Stop()
    Cursor = Cursors.Default
    EnableAllButtons()
    bgWorker.Dispose()
End Sub

Private Function MillisecondsToHMS(ByVal ms As Double) As String
    Dim ts As TimeSpan
    Dim totHrs As Integer
    Dim H, M, S, HMS As String

    ts = TimeSpan.FromMilliseconds(ms)
    totHrs = Math.Truncate(ts.TotalHours)
    H = Format(totHrs, "0#") & ":"
    M = Format(ts.Minutes, "0#") & ":"
    S = Format(ts.Seconds, "0#")
    HMS = H & M & S
    Return HMS
End Function

Private Sub tTimer_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tTimer.Tick
    lblElapsedTimeFill.Text = MillisecondsToHMS(swTimer.Elapsed.TotalMilliseconds)
    barProgress.Value = (swTimer.Elapsed.TotalSeconds / 70) * 100.0 '~70s per line
End Sub

Updating the progress bar and timer outside of the backgroundworker thread, which should have been obvious and yet somehow wasn't.


Solution

  • If, as you've stated, you don't need to tie the progress bar to the background worker, you can simply update the progress bar on the UI thread without too much difficulty. I would recommend using the System.Windows.Forms.Timer class. Every time it ticks, you can use that to update the progress bar.