UrsulRosu
UrsulRosu

Reputation: 527

Update label from mainform class with backgroundworker from another class

I have two classes.

Public Class MainForm

     Private Project As clsProject


Private Sub btnDo_Click
   ...
   Backgroundworker.RunWorkerAsync()

End Sub
 Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork


    Project = New clsProject


End Sub

and two methods inside MainForm

 Public Shared Sub setLabelTxt(ByVal text As String, ByVal lbl As Label)
    If lbl.InvokeRequired Then
        lbl.Invoke(New setLabelTxtInvoker(AddressOf setLabelTxt), text, lbl)
    Else
        lbl.Text = text
    End If
End Sub
Public Delegate Sub setLabelTxtInvoker(ByVal text As String, ByVal lbl As Label)
end class

I want to update the labels of MainForm from the clsProject constructor.

 MainForm.setLabelTxt("Getting prsadasdasdasdasdry..", MainForm.lblProgress)

but it does not update them. What am I doing wrong?

Upvotes: 3

Views: 5180

Answers (3)

user2480047
user2480047

Reputation:

You cannot execute any action on GUI-elements from the BackgroundWorker directly. One way to "overcome" that is by forcing the given actions to be performed from the main thread via Me.Invoke; but this is not the ideal proceeding. Additionally, your code mixes up main form and external class (+ shared/non-shared objects) what makes the whole structure not too solid.

A for-sure working solution is relying on the specific BGW methods for dealing with GUI elements; for example: ProgressChanged Event. Sample code:

Public Class MainForm
    Private Project As clsProject
    Public Shared bgw As System.ComponentModel.BackgroundWorker

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        bgw = BackgroundWorker1 'Required as far as you want to called it from a Shared method

        BackgroundWorker1.WorkerReportsProgress = True
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Project = New clsProject
    End Sub

    Public Shared Sub setLabelTxt(ByVal text As String)
        bgw.ReportProgress(0, text) 'You can write any int as first argument as far as will not be used anyway
    End Sub

    Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
        Me.Label1.Text = e.UserState 'You can access the given GUI-element directly
        Me.Label1.Update()
    End Sub
End Class

Public Class clsProject
    Public Sub New()
        MainForm.setLabelTxt("Getting prsadasdasdasdasdry..")
    End Sub
End Class

Upvotes: 1

Steven Doggart
Steven Doggart

Reputation: 43743

The problem is that you are using the global MainForm instance to access the label in a background thread here:

Public Class clsProject
    Public Sub New()
        ' When accessing MainForm.Label1 on the next line, it causes an exception
        MainForm.setLabelTxt("HERE!", MainForm.Label1)
    End Sub
End Class

It's OK to call MainForm.setLabelTxt, since that is a shared method, so it's not going through the global instance to call it. But, when you access the Label1 property, that's utilizing VB.NET's trickery to access the global instance of the form. Using the form through that auto-global-instance variable (which always shares the same name as the type) is apparently not allowed in non-UI threads. When you do so, it throws an InvalidOperationException, with the following error message:

An error occurred creating the form. See Exception.InnerException for details. The error is: ActiveX control '8856f961-340a-11d0-a96b-00c04fd705a2' cannot be instantiated because the current thread is not in a single-threaded apartment.

I'm guessing that the reason you are not seeing the error is because you are catching the exception somewhere and you are simply ignoring it. If you stop using that global instance variable, the error goes away and it works. For instance, if you change the constructor to this:

Public Class clsProject
    Public Sub New(f As MainForm)
        ' The next line works because it doesn't use the global MainForm instance variable
        MainForm.setLabelTxt("HERE!", f.Label1)
    End Sub
End Class

Then, in your MainForm, you would have to call it like this:

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    Project = New clsProject(Me)   ' Must pass Me
End Sub

Using the global instance from the background thread is not allowed, but when we use the same label from the background thread, without going through that global variable it works.

So it's clear that you cannot use the global MainForm variable from a background thread, but what may not be clear is that it's a bad idea to use it ever. First, it's confusing because it shares the same name as the MainForm type. More importantly, though, it is a global variable, and global state of any kind is almost always bad practice, if it can be avoided.

While the above example does solve the problem, it's still a pretty poor way of doing it. A better option would be to pass the setLabelTxt method to the clsProject object or even better have the clsProject simply raise an event when the label needs to be changed. Then, the MainForm can simply listen for those events and handle them when they happen. Ultimately, that clsProject class is probably some sort of business class which shouldn't be doing any kind of UI work anyway.

Upvotes: 2

SysDragon
SysDragon

Reputation: 9888

Try:

Me.Invoke(...)

instead of lbl.Invoke(.... I had to do this. This is my implementation:

Delegate Sub SetTextDelegate(ByVal args As String)

Private Sub SetTextBoxInfo(ByVal txt As String)
    If txtInfo.InvokeRequired Then
        Dim md As New SetTextDelegate(AddressOf SetTextBoxInfo)
        Me.Invoke(md, txt)
    Else
        txtInfo.Text = txt
    End If
End Sub

And this worked for me.

Upvotes: 1

Related Questions