updating form from backgroundworker

Ron Miel

Member
Joined
Apr 9, 2011
Messages
16
Programming Experience
10+
I can't get my backgroundworker to update my form.

This is just a test, trying to learn HOW to do it before I use it for anything serious.

I can run a procedure from the main thread, it works and updates the form, but locks up all the other controls while running.

I can run an identical procedure from a backgroundworker. I can step through it and see it working, but it doesn't update the form.

The only difference between the two is the second uses reportprogress.

=========

My form has 2 command buttons
- name= cmd_Start, text = "Start"
- name = cmd_thread, text = "Asynch"

It has three labels
- lbl_HH, lbl_MM, lbl_SS

It has one backgroundworker
- name = bgw_1, workerreportsprogress=true, workersupportscancellation=true



Code for the form
VB.NET:
 Public Class Form1

    Private Sub cmd_start_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_start.Click
        Dim my_clock As New Class_Clock
        my_clock.run_Clock()
    End Sub

    Private Sub cmd_thread_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_thread.Click
        start_thread()
    End Sub

    Private Sub BackgroundWorker1_DoWork(
                ByVal sender As System.Object,
                ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgw_1.DoWork
        Dim my_clock As Class_Clock = CType(e.Argument, Class_Clock)
        my_clock.run_Clock_asynch()
    End Sub

    Private Sub BackgroundWorker1_ProgressChanged(
        ByVal sender As System.Object,
        ByVal e As System.ComponentModel.ProgressChangedEventArgs
    ) Handles bgw_1.ProgressChanged

        Dim state As Class_Clock.CurrentState = CType(e.UserState, Class_Clock.CurrentState)

        lbl_HH.Text = state.Hours
        lbl_HH.Refresh()
        lbl_MM.Text = state.Minutes
        lbl_MM.Refresh()
        lbl_SS.Text = state.Seconds
        lbl_SS.Refresh()

    End Sub
End Class


Module code
VB.NET:
Module Module_1
    Sub start_thread()
        Dim my_clock As New Class_Clock
        Form1.bgw_1.RunWorkerAsync(my_clock)
    End Sub
End Module

Class code
VB.NET:
 Public Class Class_Clock
    Public Class CurrentState
        Public Hours As Integer
        Public Minutes As Integer
        Public Seconds As Integer
    End Class

    Public Sub run_Clock()

        Dim s As New CurrentState

        For s.Hours = 0 To 24
            Form1.lbl_HH.Text = s.Hours
            Form1.lbl_HH.Refresh()
            For s.Minutes = 0 To 59
                Form1.lbl_MM.Text = s.Minutes
                Form1.lbl_MM.Refresh()
                For s.Seconds = 0 To 59
                    Form1.lbl_SS.Text = s.Seconds
                    Form1.lbl_SS.Refresh()
                    Threading.Thread.Sleep(10)
                Next 'seconds
            Next 'minutes
        Next 'hours
    End Sub

    Public Sub run_Clock_asynch()

        Dim s As New CurrentState

        For s.Hours = 0 To 24
            Form1.lbl_HH.Text = s.Hours
            Form1.lbl_HH.Refresh()
            For s.Minutes = 0 To 59
                Form1.lbl_MM.Text = s.Minutes
                Form1.lbl_MM.Refresh()
                For s.Seconds = 0 To 59
                    Form1.lbl_SS.Text = s.Seconds
                    Form1.lbl_SS.Refresh()
                    Threading.Thread.Sleep(10)
                    Form1.bgw_1.ReportProgress(0, s)
                Next 'seconds
            Next 'minutes
        Next 'hours
    End Sub 'run_Clock_asynch
End Class
 

elroyskimms

Member
Joined
Jul 10, 2015
Messages
19
Programming Experience
10+
Working Example

Ron,

I found a couple of problems. First, you are attempting to directly change the user controls on your form inside the asynch code, which will not work.

From MSDN: "You must be careful not to manipulate any user-interface objects in your DoWork event handler. Instead, communicate to the user interface through the ProgressChanged and RunWorkerCompleted events."
https://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker(v=vs.110).aspx

Second, and I believe this is related to the first issue, you refer to Form1.bgw_1 (the backgroundworker) in the async code as well, but your class definition is outside of the Form1 class. If you want your Clock class to be reusable on other forms, you can't call Form1.bgw_1.ReportProgress from within the class. What you need to do is add a backgroundworker property to your Clock class, and then pass a reference to bgw_1 to the Clock instance.

I made some changes to the code and I believe it works as expected now. You will notice that I also moved the code that updates the labels on the form. Even your non-asynch code was referencing the Form1 label controls, which is something you want to avoid. So I added 3 label properties to your Clock definition.

If you have any questions, let me know. I'm sure there are more efficient ways of doing this, but I wanted to get you something that works and see if it makes sense to you.

-E

VB.NET:
Imports System.ComponentModel

Public Class Form1
    Private Sub cmd_start_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_Start.Click
        Dim my_clock As New Class_Clock(lbl_HH, lbl_MM, lbl_SS)
        my_clock.run_Clock()
    End Sub

    Private Sub cmd_thread_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_thread.Click
        Dim my_clock As New Class_Clock(lbl_HH, lbl_MM, lbl_SS, bgw_1)
        bgw_1.RunWorkerAsync(my_clock)
    End Sub

    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgw_1.DoWork
        Dim my_clock As Class_Clock = CType(e.Argument, Class_Clock)
        my_clock.run_Clock_asynch()
    End Sub

    Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles bgw_1.ProgressChanged
        With CType(e.UserState, Class_Clock)
            .UpdateLabels()
        End With
    End Sub

End Class

Public Class Class_Clock

    Public Class CurrentState
        Public Hours As Integer
        Public Minutes As Integer
        Public Seconds As Integer
    End Class

    Private _bgWorker As BackgroundWorker
    Public Property bgWorker() As BackgroundWorker
        Get
            Return _bgWorker
        End Get
        Set(ByVal value As BackgroundWorker)
            _bgWorker = value
        End Set
    End Property

    Private _lblHour As Label
    Public Property lblHour() As Label
        Get
            Return _lblHour
        End Get
        Set(ByVal value As Label)
            _lblHour = value
        End Set
    End Property

    Private _lblMinute As Label
    Public Property lblMinute() As Label
        Get
            Return _lblMinute
        End Get
        Set(ByVal value As Label)
            _lblMinute = value
        End Set
    End Property

    Private _lblSecond As Label
    Public Property lblSecond() As Label
        Get
            Return _lblSecond
        End Get
        Set(ByVal value As Label)
            _lblSecond = value
        End Set
    End Property

    Private _State As CurrentState
    Public Property State() As CurrentState
        Get
            Return _State
        End Get
        Set(ByVal value As CurrentState)
            _State = value
        End Set
    End Property

    Sub New(ilblHour As Label, ilblMinute As Label, ilblSecond As Label)
        MyBase.New()
        State = New CurrentState
        lblHour = ilblHour
        lblMinute = ilblMinute
        lblSecond = ilblSecond
    End Sub

    Sub New(ilblHour As Label, ilblMinute As Label, ilblSecond As Label, ibgWorker As BackgroundWorker)
        MyBase.New()
        State = New CurrentState
        lblHour = ilblHour
        lblMinute = ilblMinute
        lblSecond = ilblSecond
        bgWorker = ibgWorker
    End Sub

    Public Sub run_Clock()
        For State.Hours = 0 To 24
            For State.Minutes = 0 To 59
                For State.Seconds = 0 To 59
                    Threading.Thread.Sleep(10)
                    UpdateLabels()
                Next 'seconds
            Next 'minutes
        Next 'hours
    End Sub

    Public Sub run_Clock_asynch()
        For State.Hours = 0 To 24
            For State.Minutes = 0 To 59
                For State.Seconds = 0 To 59
                    Threading.Thread.Sleep(10)
                    bgWorker.ReportProgress(0, Me)
                Next 'seconds
            Next 'minutes
        Next 'hours
    End Sub 'run_Clock_asynch

    Sub UpdateLabels()
        lblHour.Text = State.Hours
        lblHour.Refresh()
        lblMinute.Text = State.Minutes
        lblMinute.Refresh()
        lblSecond.Text = State.Seconds
        lblSecond.Refresh()
    End Sub

End Class
 

Ron Miel

Member
Joined
Apr 9, 2011
Messages
16
Programming Experience
10+
Elroy,

Thank you for your reply. However, you've given me rather more than I wanted. And that has made it a little hard to follow.

I'm not actually trying to write a clock application. I'm just trying to learn how to use a backgroundworker. The clock is just a simple and entirely arbitrary widget. It's just something that changes frequently, and runs for any length of time. It could have been anything at all. I don't need it to be transferable to other forms, for instance.

I have done some object oriented programming, but I'm far more experienced with modular code. Is it even necessary to use a class at all? (The examples I found online did.)

Could you just show a very basic backgroundworker. Leave out all unnecessary stuff. I can study proper use of classes another day.
 

jmcilhinney

VB.NET Forum Moderator
Staff member
Joined
Aug 17, 2004
Messages
14,603
Location
Sydney, Australia
Programming Experience
10+
Using a BackgroundWorker is easy if you understand the basic principles. The whole point of using a BackgroundWorker is to do background work. The background work gets done in the aptly-named DoWork event handler. Anything to do with the UI is inherently foreground work - the UI is the very definition of the foreground - so you never touch the UI in the DoWork event handler.

So, you call RunWorkerAsync to start the ball rolling. That causes the BackgroundWorker to raise its DoWork event so you handle that event and do your work in the event handler. That method is executed on a secondary thread, so it can happen while the UI thread is looking after the UI.

If you need to update the UI during the background work, you call the ReportProgress method of the BackgroundWorker. That causes the ProgressChanged event to be raised so you handle that event and update the UI in the event handler. That method is executed on the UI thread, so there's no issue accessing UI elements.

If you need to update the UI when the background work has completed then you handle the RunWorkerCompleted event. That event handler is also executed on the UI thread so there is no issue accessing UI elements.

For more info and examples, check this out:

Using the BackgroundWorker Component
 

elroyskimms

Member
Joined
Jul 10, 2015
Messages
19
Programming Experience
10+
Updated Sample

Ron,

I apologize for the confusion. I wasn't very clear in my explanation. The problems I pointed out are what was preventing the backgroundworker from updating the form. My goal was to answer your question as to why your code wasn't working as intended. At bare minimum, your code was not working because you were calling Form1.bgw_1 from outside the Form. To make your original code work, you need to change your cmd_thread_Click method to:

VB.NET:
    Private Sub cmd_thread_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_thread.Click
        start_thread(bgw_1)
    End Sub

The start_thread method in Module_1 needs to be:
VB.NET:
    Sub start_thread(ibgWorker As BackgroundWorker)
        Dim my_clock As New Class_Clock(ibgWorker)
        ibgWorker.RunWorkerAsync(my_clock)
    End Sub

Your Clock_Class needs to have a backgroundworker property:
VB.NET:
    Private _bgWorker As BackgroundWorker
    Public Property bgWorker() As BackgroundWorker
        Get
            Return _bgWorker
        End Get
        Set(ByVal value As BackgroundWorker)
            _bgWorker = value
        End Set
    End Property
    Sub New()
        MyBase.New()
    End Sub

    Sub New(ibgWorker As BackgroundWorker)
        MyBase.New()
        bgWorker = ibgWorker
    End Sub

and the ReportProgress call in run_Clock_asynch needs to be changed to:
VB.NET:
                    bgWorker.ReportProgress(0, s)

Do this, and your original code will work. Accessing Form1 controls outside of the Form1 class (especially on another thread) was causing the problem you were having. The other change I made (taking all UI control updates out of the backgroundworker code) wasn't necessary to make your sample code run, but it is necessary when you try and take what you learned here and use it in another project. When multiple threads try and update the same UI controls, bad things happen. One solution for that is to use a Delegate and CallBack, but that's beyond the scope of your question. The best way to answer your question without delving into Delegates and CallBacks, was to move the UI code to the proper place. It's not that I was trying to give you a proper study of classes, it's that I was trying to get your code to work and make sure you knew that touching the UI outside the main thread is a bad idea and will lead to all sorts of problems in the future. Trust me, I've been there, done that. You don't want to do it.

As to your request for a very simple backgroundworker example, you will find one below. Create a form with 2 buttons: btnStart & btnCancel, a progressbar: pbBackground, and a backgroundworker: bgSample (ReportsProgress: True, SupportsCancellation: True)

VB.NET:
Imports System.ComponentModel
Public Class Form1

    Private Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
        bgSample.RunWorkerAsync()
    End Sub

    Private Sub SampleTask(sender As Object, e As DoWorkEventArgs) Handles bgSample.DoWork
        For i As Double = 0 To 1000
            If bgSample.CancellationPending Then
                MessageBox.Show("Cancel request received. ", "Cancel", MessageBoxButtons.OK)
                e.Cancel = True
                Exit Sub
            End If
            bgSample.ReportProgress((i / 1000) * 100)
            Threading.Thread.Sleep(10)
        Next
    End Sub

    Private Sub ProgressBarUpdate(sender As Object, e As ProgressChangedEventArgs) Handles bgSample.ProgressChanged
        pbBackground.Value = e.ProgressPercentage
    End Sub

    Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
        bgSample.CancelAsync()
    End Sub

    Private Sub bgSample_WorkComplete(sender As Object, e As RunWorkerCompletedEventArgs) Handles bgSample.RunWorkerCompleted
        If Not e.Cancelled Then MessageBox.Show("The backgroundworker completed the task.", "Work Complete", MessageBoxButtons.OK)
    End Sub

End Class

I hope this helps.

-E
 

Ron Miel

Member
Joined
Apr 9, 2011
Messages
16
Programming Experience
10+
Okay, thanks guys. Following your advice I've got it to work.

Realising that I didn't need to use a class made it a lot simpler.

VB.NET:
Public Class Form1

    Public Class Time_object
        Public Hours As Integer
        Public Minutes As Integer
        Public Seconds As Integer
    End Class

    Private Sub cmd_thread_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_thread.Click
        bgw_1.RunWorkerAsync()
    End Sub

    Private Sub bgw_1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgw_1.DoWork
        Dim hours, minutes, seconds As Integer
        Dim the_time As String
        Dim time1 As New Time_object

        For hours = 0 To 24
            time1.Hours = hours
            For minutes = 0 To 59
                time1.Minutes = minutes
                For seconds = 0 To 59
                    time1.Seconds = seconds
                    the_time = hours.ToString + ":" + minutes.ToString + ":" + seconds.ToString
                    sender.ReportProgress(0, time1)
                    Threading.Thread.Sleep(10)
                Next 'seconds
            Next 'minutes
        Next 'hours
    End Sub

    Private Sub bgw_1_ProgressChanged(
    ByVal sender As System.Object,
    ByVal e As System.ComponentModel.ProgressChangedEventArgs
    ) Handles bgw_1.ProgressChanged
        Dim Time1 As Time_object = CType(e.UserState, Time_object)
        Me.lbl_HH.Text = Time1.Hours
        Me.lbl_MM.Text = Time1.Minutes
        Me.lbl_SS.Text = Time1.Seconds
    End Sub

End Class
 

Ron Miel

Member
Joined
Apr 9, 2011
Messages
16
Programming Experience
10+
Okay, next question.

To update the form, do I have to pass the value of every control every time?

Can I just say update this one control with this value?
 

elroyskimms

Member
Joined
Jul 10, 2015
Messages
19
Programming Experience
10+
No, passing the control is actually not a good idea. I threw it out there earlier to make your original code work with as few changes as possible. It wasn't pretty, and it wasn't any sort of best practice.

If you are using a backgroundworker, put the UI code in the ProgressChanged event handler. The backgroundworker will continue working in the DoWork code, and the ProgressChanged event will signal the primary thread to update the UI.

-E
 

jmcilhinney

VB.NET Forum Moderator
Staff member
Joined
Aug 17, 2004
Messages
14,603
Location
Sydney, Australia
Programming Experience
10+
No, passing the control is actually not a good idea. I threw it out there earlier to make your original code work with as few changes as possible. It wasn't pretty, and it wasn't any sort of best practice.

If you are using a backgroundworker, put the UI code in the ProgressChanged event handler. The backgroundworker will continue working in the DoWork code, and the ProgressChanged event will signal the primary thread to update the UI.

-E

That's true, but the ProgressChanged event handler has to know WHAT to update. If it's the same thing every time, e.g. a ProgressBar, then there's no need to specify. If you want to update different things in different ways at different times then you have to specify.

When calling ReportProgress, the second parameter is called "userState" and is type Object. It can be absolutely anything you want. If you want to update different controls in different ways at different times then you need to pass an object to that parameter that contains sufficient information for the code in the ProgressChanged event handler to determine exactly what it needs to do. That might well include a reference to the control to be updated and there's no issue with that. There's no problem referring to a control in the DoWork event handler in order to pass it to the ReportProgress method. What you can't do is invoke a member of that control.
 
Top Bottom