Keith Keith - 4 months ago 22
Vb.net Question

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.

Answer

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.

Comments