VB.Net - How to update a UI ProgressBar from a worker thread?

kepler

New member
Afaik you shouldn't (possibly cannot) modify a control from one (e.g. worker) thread that was created on another (e.g. UI thread).

I may be able to hammer-and-tongs it by disabling the check on illegal cross thread calls but obviously this introduces an element of flaky design.

Is there any simple convention for solving this worker thread progress problem without losing multi-threading or cross thread call checking?
 
Gah, I was afraid of that. I just can't get my head around delegates for some reason, I'm yet to see a non-gibberish explanation of them. I'll give the msdn one a go, cheers.
 
If you can explain this
I am going to start by examining the code behind the Windows Form named Form1 (see Figure 1). This code demonstrates how to execute a method asynchronously. The technique involves binding a target method (GetCustomerList) to a delegate object and then calling BeginInvoke on that delegate object to start the method's asynchronous execution.

The technique used here also shows how to bind a callback method (specifically, MyCallbackMethod) to a second delegate object. A reference to this delegate object is then passed as the second parameter in the call to BeginInvoke:
Why these people have to write like you have intimate knowledge of every bit of code they've ever written, I don't know. Useless.
 
Last edited:
Grr. I went through that MSDN code and after all that the worker thread just sleeps and then returns an array before quitting.

Oh well, this may actually solve an intermittent bug I was having with final output but I'm still in the dark as to how you'd communicate with the UI while the worker thread carries on (i.e. updating work progress information from the worker thread), one step at a time I suppose.
 
If you can explain this Why these people have to write like you have intimate knowledge of every bit of code they've ever written, I don't know. Useless.

Amen, some of the stuff on MSDN assumes you wrote the CLR.

I was trying to dynamically load a user control, and pass in some params to it's constructor. Of course they buggered this in 2.0 because they essentially did away with the web project. All you really have to do it place a reference to the control you want to load on the aspx page. Do you think that's what they said on MSDN? Of course not.
 
This is starting to annoy me now.

I have 2 classes

frmUI.vb (UI form)

Code:
mports System.Threading

Public Class frmUI
    Inherits Form

    Dim backgroundThread As Thread
    Dim worker As New clsWorker

    ' UI MAINTENANCE ------------------------------

    Private Sub frmUI_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'Sets the delegate instance to the method implementation 

        worker.ProgressChangedHandler = New clsWorker.ProgressChanged(AddressOf Me.WorkerProgressChanged)

        backgroundThread = New Thread(AddressOf Me.BackgroundProcessCaller)
    End Sub

    Private Sub frmUI_Closing(ByVal sender As System.Object, ByVal e As Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing
        If backgroundThread IsNot Nothing Then
            If backgroundThread.IsAlive Then
                backgroundThread.Abort()
            End If
        End If
        backgroundThread = Nothing
    End Sub

    ' UI EVENTS --------------------------------------

    Private Sub btnBkgGo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnBkgGo.Click
        backgroundThread.Name = "Worker Thread"
        backgroundThread.IsBackground = True
        backgroundThread.Start()
    End Sub

    Private Sub BackgroundProcessCaller()
        worker.work()
    End Sub

    Public Sub WorkerProgressChanged(ByVal progress As Integer)
        rtbOut.Text &= progress & " - "
    End Sub

End Class

and clsWorker.vb (Worker thread)

Code:
Public Class clsWorker

    Public Delegate Sub ProgressChanged(ByVal progress As Integer)

    Public ProgressChangedHandler As ProgressChanged

    Public Sub work()
        ' In here we will do work, alerting the UI at intervals
        For i As Integer = 1 To 100
            System.Threading.Thread.Sleep(500)
            If Not Me.ProgressChangedHandler Is Nothing Then
                Me.ProgressChangedHandler(i)
            End If
        Next
    End Sub

End Class

Stepping through, it goes through it correctly, steps from Me.ProgressChangedHandler(i) to Public Sub WorkerProgressChanged(ByVal progress As Integer) as it should but when it tries to access the UI control (RichTextBox) I get the cross thread call exception.

I must be doing something wrong but I'll be damned if I know what. :mad:
 
Looks like I've got it working in a demo app. I'm passing in a reference to the UI into the worker class constructor which is ringing bad-design bells, but other than that it appears to work. :up:

frmUI.vb

Code:
Imports System
Imports System.ComponentModel
Imports System.Threading
Imports System.Windows.Forms

Public Class frmUI
    Inherits Form

    Delegate Sub SetTextCallback(ByVal [text] As String)

    Private demoThread As Thread = Nothing
    Private WithEvents backgroundWorker1 As BackgroundWorker
    Friend WithEvents setTextSafeBtn As System.Windows.Forms.Button
    Private components As System.ComponentModel.IContainer = Nothing
    Friend WithEvents prgBar As System.Windows.Forms.ProgressBar
    Friend WithEvents lblProgress As System.Windows.Forms.Label

    Private worker As clsWorker

    Public Sub New()
        InitializeComponent()
        worker = New clsWorker(Me)
        prgBar.Maximum = 100
        prgBar.Minimum = 0
    End Sub

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing AndAlso (components IsNot Nothing) Then
            components.Dispose()
        End If
        MyBase.Dispose(disposing)
    End Sub

    Private Sub setTextSafeBtn_Click(ByVal sender As Object, ByVal e As EventArgs) Handles setTextSafeBtn.Click
        demoThread = New Thread(New ThreadStart(AddressOf worker.work))
        demoThread.Start()
    End Sub

    Friend Sub UpdateUI(ByVal [text] As String)

        ' InvokeRequired required compares the thread ID of the
        ' calling thread to the thread ID of the creating thread.
        ' If these threads are different, it returns true.
        If Me.lblProgress.InvokeRequired Then
            Dim d As New SetTextCallback(AddressOf UpdateUI)
            Me.Invoke(d, New Object() {[text]})
        Else
            Me.lblProgress.Text = [text]
            Me.prgBar.Increment(1)
        End If
    End Sub


#Region "Windows Form Designer generated code"


    Private Sub InitializeComponent()
        Me.backgroundWorker1 = New System.ComponentModel.BackgroundWorker
        Me.setTextSafeBtn = New System.Windows.Forms.Button
        Me.prgBar = New System.Windows.Forms.ProgressBar
        Me.lblProgress = New System.Windows.Forms.Label
        Me.SuspendLayout()
        '
        'setTextSafeBtn
        '
        Me.setTextSafeBtn.Location = New System.Drawing.Point(12, 28)
        Me.setTextSafeBtn.Name = "setTextSafeBtn"
        Me.setTextSafeBtn.Size = New System.Drawing.Size(81, 23)
        Me.setTextSafeBtn.TabIndex = 4
        Me.setTextSafeBtn.Text = "Run Process"
        Me.setTextSafeBtn.UseVisualStyleBackColor = True
        '
        'prgBar
        '
        Me.prgBar.Location = New System.Drawing.Point(12, 12)
        Me.prgBar.Name = "prgBar"
        Me.prgBar.Size = New System.Drawing.Size(239, 13)
        Me.prgBar.Style = System.Windows.Forms.ProgressBarStyle.Continuous
        Me.prgBar.TabIndex = 5
        '
        'lblProgress
        '
        Me.lblProgress.AutoSize = True
        Me.lblProgress.Location = New System.Drawing.Point(185, 28)
        Me.lblProgress.Name = "lblProgress"
        Me.lblProgress.Size = New System.Drawing.Size(0, 13)
        Me.lblProgress.TabIndex = 6
        '
        'frmUI
        '
        Me.ClientSize = New System.Drawing.Size(268, 60)
        Me.Controls.Add(Me.lblProgress)
        Me.Controls.Add(Me.prgBar)
        Me.Controls.Add(Me.setTextSafeBtn)
        Me.Name = "frmUI"
        Me.Text = "Form1"
        Me.ResumeLayout(False)
        Me.PerformLayout()

    End Sub 'InitializeComponent 

#End Region

    <STAThread()> _
    Shared Sub Main()
        Application.EnableVisualStyles()
        Application.Run(New frmUI())
    End Sub
End Class

clsWorker.vb

Code:
Public Class clsWorker

    Private m_frm As frmUI

    Public Sub New(ByRef p_form As Form)
        m_frm = p_form
    End Sub

    Public Sub work()
        ' In here we will do work, alerting the UI at intervals
        For i As Integer = 1 To 100
            System.Threading.Thread.Sleep(100)
            m_frm.UpdateUI(i & "% complete")
        Next
    End Sub

End Class
 
For ease of use, stripped down it looks like this (with all the designer guff removed(

Code:
Imports System.Threading

Public Class frmMain
    Inherits Form

    Delegate Sub SetTextCallback(ByVal [text] As String)

    Private workerThread As Thread
    Private worker As clsWorker

    Private Sub frmMain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        worker = New clsWorker(Me)
    End Sub

    Private Sub btnDoWork_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDoWork.Click
        ' If work has not begun
        If workerThread Is Nothing Then
            ' Setup and start the worker thread
            workerThread = New Thread(New ThreadStart(AddressOf worker.work))
            workerThread.Start()
        End If
    End Sub

    ' This will be called from the worker thread to set
    ' the work's progress
    Friend Sub UpdateUI(ByVal [text] As String)
        If Me.lblComplete.InvokeRequired Then
            Dim _delegate As New SetTextCallback(AddressOf UpdateUI)
            Me.Invoke(_delegate, New Object() {[text]})
        Else
            ' Increment the bar's display
            Me.progressBar.Increment(1)
            ' Display the % work done
            Me.lblComplete.Text = ((CInt([text]) / progressBar.Maximum) * 100) & "% Complete"
        End If
    End Sub

End Class ' frmMain

Code:
Public Class clsWorker

    Private m_frm As frmMain

    Public Sub New(ByRef p_form As frmMain)
        m_frm = p_form
    End Sub

    Public Sub work()
        ' Sets the max value of the UIs ProgressBar
        setProgressBarMaximum()
        ' In here we will do work, alerting the UI at intervals
        For i As Integer = 1 To 100
            System.Threading.Thread.Sleep(100)
            m_frm.UpdateUI(i)
        Next
    End Sub

End Class ' clsWorker

To make it actually useful you can easily extend the code to allow the worker thread to set the maximum of the progress bar, as hard coding it is a bit daft. I've just stripped all the non-essential stuff out for easy reading. If anyone has a more elegant vb.net solution, shout up. :D
 
Back
Top