Reputation: 33
Long time reader, first time poster. Usually I'm able to find the answer and make it work. Not this time..... I'm using VB.NET in VS2013. I am trying to update a progress bar with work done in a secondary thread. Easy right? No. I had to make it more complicated. The progress bar (ToolStripProgressBar1) is on the main form (frmMain), the MDI of the project. A secondary form (frmShipping) has a button which initiates a second thread to do some COMM Port communications in a class (cApex). I can get the progress bar to update on the frmMain from the main UI thread (frmShipping button).
This is the code from button on frmShiping and the multithread procedure:
Private Sub btnreadScanner_Click(sender As Object, e As EventArgs) Handles btnreadScanner.Click
Dim thrReadScanner As New System.Threading.Thread(AddressOf ReadScanner)
thrReadScanner.IsBackground = True
thrReadScanner.Start()
End Sub
Private Sub ReadScanner()
Dim strRowCount As String
ShipmentMsg(2)
strRowCount = objShipping.RecordsExisit.ToString()
Try
objApex.ImmediateMode()
If objApex.FileDownload = False Then
Throw New Exception(Err.Description)
End If
Catch ex As Exception
ShipmentMsg(1)
MessageBox.Show("No Data downloaded from Scanner. Try Again. Error#: " & Err.Number & " : " & Err.Description)
Exit Sub
End Try
RecordCount()
DataGridUpdate()
btnProcessShipment.Enabled = True
ShipmentMsg(5)
ScanErrors()
End Sub
This all works great. As expected. The call to objApex.FileDownload in class cApex is where progress bar is to be updated from (actually in another function called from FileDownload). So here is the code there.
Try
GetHeaderRecord()
If Count <> 0 Then intTicks = Math.Round((100 / Count), 1)
For intcount As Integer = 1 To Count
Dim intLength As Integer = Length
Do While intLength > 0
literal = Chr(_serialPort.ReadChar.ToString)
If literal = ">" Then Exit Do
strRecord = strRecord & literal
intLength = intLength - 1
Loop
REF = strRecord.Substring(0, 16).TrimEnd
SKID = strRecord.Substring(16, 16).TrimEnd
REEL_BC = strRecord.Substring(32, 15).TrimEnd
ScanDate = strRecord.Substring(47, 8).TrimEnd
ScanDate = DateTime.ParseExact(ScanDate, "yyyyMMdd", Nothing).ToString("MM/dd/yyyy")
ScanTime = strRecord.Substring(55, 6).TrimEnd
ScanTime = DateTime.ParseExact(ScanTime, "HHmmss", Nothing).ToString("HH:mm:ss")
strRecordTotal = strRecordTotal & strRecord & CRLF
Dim strSQL As String
strSQL = "INSERT INTO tblScanData (PONo,Barcode,SkidNo,ScanDate,ScanTime) " & _
"VALUES (" & _
Chr(39) & REF & Chr(39) & _
"," & Chr(39) & REEL_BC & Chr(39) & _
"," & Chr(39) & SKID & Chr(39) & _
"," & Chr(39) & ScanDate & Chr(39) & _
"," & Chr(39) & ScanTime & Chr(39) & ")"
objData.Executecommand(strSQL)
strRecord = ""
Next
And finally this is how I was calling the progress bar update.
Dim f As frmMain = frmMain
System.Threading.Thread.Sleep(100)
DirectCast(f, frmMain).ToolStripProgressBar1.PerformStep()
I really need to put the PerformStep in the For loop. Each time around the loop will step the progress bar the percentage of steps needed to make bar fairly accurate (done by the math code before loop). Also I setup the properties of the progress bar control on frmMain. So, am I crazy, or is there a way to accomplish this? I tried using a delegate; Me.Invoke(New MethodInvoker(AddressOf pbStep)) to make code cross thread safe. I don't get an error about cross thread calls, but the progress bar doesn't update either. Sorry it's a long one but I'm lost and my ADHD won't let me scrap this idea.
EDIT AS REQUESTED:
Public Sub pbStep()
Dim f As frmMain = frmMain
If Me.InvokeRequired Then
Me.Invoke(New MethodInvoker(AddressOf pbStep))
Else
DirectCast(f, frmMain).ToolStripProgressBar1.PerformStep()
System.Threading.Thread.Sleep(100)
End If
End Sub
Upvotes: 1
Views: 3759
Reputation: 33
Both responses helped lead me to the correct answer I was needing. The code provided by James was a great starting point to build on, and Hans has several post explaining the BackgroundWorker. I wanted to share the "Answer" I came up with. I'm not saying its the best way to do this, and I'm sure I'm violating some rules of common logic. Also, a lot of the code came from a MSDN example and James's code.
Lets start with the form from which I am calling the bgw, frmShipping. I added this code:
Dim WithEvents bgw1 As New System.ComponentModel.BackgroundWorker
Private Sub bgw1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles bgw1.RunWorkerCompleted
If e.Error IsNot Nothing Then
MessageBox.Show("Error: " & e.Error.Message)
ElseIf e.Cancelled Then
MessageBox.Show("Process Canceled.")
Else
MessageBox.Show("Finished Process.")
End If
End Sub
Private Sub bgw1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
Handles bgw1.ProgressChanged
DirectCast(Me.MdiParent, frmMain).ToolStripProgressBar1.Maximum = 1960
DirectCast(Me.MdiParent, frmMain).ToolStripProgressBar1.Step = 2
Dim state As cApex.CurrentState =
CType(e.UserState, cApex.CurrentState)
DirectCast(Me.MdiParent, frmMain).txtCount.Text = state.LinesCounted.ToString
DirectCast(Me.MdiParent, frmMain).txtPercent.Text = e.ProgressPercentage.ToString
DirectCast(Me.MdiParent, frmMain).ToolStripProgressBar1.PerformStep()
End Sub
Private Sub bgw1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) _
Handles bgw1.DoWork
Dim worker As System.ComponentModel.BackgroundWorker
worker = CType(sender, System.ComponentModel.BackgroundWorker)
Dim objApex As cApex = CType(e.Argument, cApex)
objApex.CountLines(worker, e)
End Sub
Sub StartThread()
Me.txtCount.Text = "0"
Dim objApex As New cApex
bgw1.WorkerReportsProgress = True
bgw1.RunWorkerAsync(objApex)
End Sub
Next I added the following code the my cApex class.
Public Class CurrentState
Public LinesCounted
End Class
Private LinesCounted As Integer = 0
Public Sub CountLines(ByVal worker As System.ComponentModel.BackgroundWorker, _
ByVal e As System.ComponentModel.DoWorkEventArgs)
Dim state As New CurrentState
Dim line = ""
Dim elaspedTime = 20
Dim lastReportDateTime = Now
Dim lineCount = File.ReadAllLines(My.Settings.strGenFilePath).Length
Dim percent = Math.Round(100 / lineCount, 2)
Dim totaldone As Double = 2
Using myStream As New StreamReader(My.Settings.strGenFilePath)
Do While Not myStream.EndOfStream
If worker.CancellationPending Then
e.Cancel = True
Exit Do
Else
line = myStream.ReadLine
LinesCounted += 1
totaldone += percent
If Now > lastReportDateTime.AddMilliseconds(elaspedTime) Then
state.LinesCounted = LinesCounted
worker.ReportProgress(totaldone, state)
lastReportDateTime = Now
End If
System.Threading.Thread.Sleep(2)
End If
Loop
state.LinesCounted = LinesCounted
worker.ReportProgress(totaldone, state)
End Using
End Sub
I also added a couple of text boxes to my main form to show the current line count from the file being read from and the overall progress as a percentage of a 100. Then on the Click event of my button I just call StartThread()
. It is not 100% accurate, but its close enough to give the user a very good idea where the process stands. I have a little more work to do to add it to the "ReadScanner" function, where I originally was wanting to use the progress bar. But this function it the longer of the two that I perform on the scanner, writing almost 2,000 lines of code through a COMM Port. I'm happy with the results.
Thank you guys for helping out!
P.S. I have also now added variables to set the pbar.Maximum
and the pbar.step
since those can change if the scanner file is changed.
Upvotes: 2
Reputation: 839
Background workers are useful for this purpose. Just use it in combination with a delegate. All the threaded work is done within the DoWork event of the worker. As progress is made, progress is reported within the DoWork event. This in turn fires the ProgressedChanged event of the worker class which is on the same thread as the progressbar. Once the DoWork has completed and is out of scope, the RunWorkerCompleted event is fired. This can be used to do inform the user that the task is complete, etc. Here is a working solution that I threw together. Just paste it behind an empty form and run.
Imports System.Windows.Forms
Imports System.ComponentModel
Imports System.Threading
Public Class Form1
Private _progressBar As ProgressBar
Private _worker As BackgroundWorker
Sub New()
' This call is required by the designer.
InitializeComponent()
Initialize()
BindComponent()
End Sub
Private Sub Initialize()
_progressBar = New ProgressBar()
_progressBar.Dock = DockStyle.Fill
_worker = New BackgroundWorker()
_worker.WorkerReportsProgress = True
_worker.WorkerSupportsCancellation = True
Me.Controls.Add(_progressBar)
End Sub
Private Sub BindComponent()
AddHandler _worker.ProgressChanged, AddressOf _worker_ProgressChanged
AddHandler _worker.RunWorkerCompleted, AddressOf _worker_RunWorkerCompleted
AddHandler _worker.DoWork, AddressOf _worker_DoWork
AddHandler Me.Load, AddressOf Form1_Load
End Sub
Private Sub Form1_Load()
_worker.RunWorkerAsync()
End Sub
Private Sub _worker_ProgressChanged(ByVal o As Object, ByVal e As ProgressChangedEventArgs)
_progressBar.Increment(e.ProgressPercentage)
End Sub
Private Sub _worker_RunWorkerCompleted(ByVal o As Object, ByVal e As RunWorkerCompletedEventArgs)
End Sub
Private Sub _worker_DoWork(ByVal o As Object, ByVal e As DoWorkEventArgs)
Dim worker = DirectCast(o, BackgroundWorker)
Dim value = 10000
SetProgressMaximum(value)
For x As Integer = 0 To value
Thread.Sleep(100)
worker.ReportProgress(x)
Next
End Sub
Private Sub SetProgressMaximum(ByVal max As Integer)
If _progressBar.InvokeRequired Then
_progressBar.Invoke(Sub() SetProgressMaximum(max))
Else
_progressBar.Maximum = max
End If
End Sub
End Class
Upvotes: 1