I have two classes.
Public Class MainForm
Private Project As clsProject
Private Sub btnDo_Click
...
Backgroundworker.RunWorkerAsync()
End Sub
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.
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
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.