Mckenzie
Mckenzie

Reputation: 33

vb.net update progress bar multithread

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

Answers (2)

Mckenzie
Mckenzie

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

James Shaw
James Shaw

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

Related Questions