Update label from mainform class with backgroundworker from another class

后端 未结 3 429
[愿得一人]
[愿得一人] 2021-01-20 19:19

I have two classes.

Public Class MainForm

     Private Project As clsProject


Private Sub btnDo_Click
   ...
   Backgroundworker.RunWorkerAsync()

End Sub
         


        
相关标签:
3条回答
  • 2021-01-20 19:52

    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.

    0 讨论(0)
  • 2021-01-20 20:09

    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
    
    0 讨论(0)
  • 2021-01-20 20:10

    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.

    0 讨论(0)
提交回复
热议问题