Cross-thread operation.

Thequazi

Member
Joined
Nov 18, 2011
Messages
7
Programming Experience
Beginner
I'm setting up a simple app that has 2 variables, one from a combobox and one from a textbox.

Putting all my code directly in the button1_click sub works just fine, however, I wanted to add a marquee progress bar so I tried to implement a backgroundworker.

VB.NET:
Private WithEvents bgw As System.ComponentModel.BackgroundWorker

VB.NET:
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        ProgressBar1.Show()
        Button1.Text = "Please Wait"
        bgw = BackgroundWorker1
        bgw.WorkerSupportsCancellation = True
        bgw.RunWorkerAsync()
    End Sub

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

        Dim store As String = ComboBox1.Text & TextBox1.Text
        Try
            If My.Computer.Network.Ping(store & "p1") = True Then
                'Map a drive
                ...

I am now aware that you can not call objects like combobox directly from a backgroundworker thread.

I've tried to declare a delegate with:
VB.NET:
Public Delegate Sub SetComboBoxTextDelegate(ByVal text As String)

Create a sub:
VB.NET:
    Public Sub Store(ByVal text As String)
        Dim store As String = ComboBox1.Text & TextBox1.Text
    End Sub

and call it with
VB.NET:
Me.Invoke(New SetComboBoxTextDelegate(AddressOf SetComboBoxText), ComboBox1.Text)

but it still throws the Cross-thread operation invalid error.
What the devil am I doing wrong?
 
Let's look at this code, which is the original problem:
VB.NET:
Dim store As String = ComboBox1.Text & TextBox1.Text
What is that code doing? It is getting the Text of a ComboBox, getting the Text of a TextBox, joining them together and then setting a variable with the result. Now let's look at this code, which is part of the proposed solution:
VB.NET:
Me.Invoke(New SetComboBoxTextDelegate(AddressOf SetComboBoxText), ComboBox1.Text)
If the original code was getting the Text from a ComboBox, how can a method named SetComboBoxText and a delagate called SetComboBoxTextDelegate be a solution? That last line is passing ComboBox1.Text as an argument and getting ComboBox1.Text was the original problem and that's still there, so you have solved nothing.

Let's go back to the original code:
VB.NET:
Dim store As String = ComboBox1.Text & TextBox1.Text
What is the actual problem here? It's this part:
VB.NET:
Dim store As String = [B][U]ComboBox1.Text & TextBox1.Text[/U][/B]
You need to replace just that part:
VB.NET:
Dim store As String = XYZ
Now XYZ needs to do what the original code did, i.e. get the Text from the ComboBox, get the Text from the TextBox, join the two and return the result. It needs to get those Text properties on the UI thread. Here's a hint:
VB.NET:
Private Function GetCombinedText() As String
    Return ComboBox1.Text & TextBox1.Text
End Function
 
I suppose I need a lesson in Delegates and Invoking.

Creating a function to return the text is fine and I can understand why we would do that. However, how I can't get my head around how to invoke the main thread to get at the text in the boxes.

While using
VB.NET:
Public Delegate Sub GetCombinedTextDelegate(ByVal CombinedText As String)

and
VB.NET:
Me.Invoke(New GetCombinedTextDelegate(AddressOf GetCombinedText))

I get Paramater Count Mismatch.
I'm also not sure where I should be invoking when using the function. Should it be, where I would suspect, under the Private Function that's returning the info from the boxes?

Forgive me for all the questions. Sometimes I feel I should have gone to college.
 
I suggest that you read this:

Accessing Controls from Worker Threads

You'll see how the pattern you are using is for setting, not for getting.

This was wonderful! Who needs college =P

Perhaps you guys can clarify what's happening here. The result I'm expecting, when selecting OBS from the combo box and entering 0901 in the text box is just "OBS0901". However, it doubles up and gives me OBS0901OBS0901.

VB.NET:
Private Delegate Function GetCombinedTextInvoker() As String

VB.NET:
    Private Function GetCombinedText() As String
        Dim Concept As String
        Dim Number As String
        If Me.ComboBox1.InvokeRequired And Me.TextBox1.InvokeRequired Then
            Concept = CStr(Me.ComboBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
            Number = CStr(Me.TextBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
        Else
            Concept = Me.ComboBox1.Text
            Number = Me.TextBox1.Text
        End If

        Return Concept & Number

    End Function

VB.NET:
    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Dim store As String = GetCombinedText()

        MsgBox(store)
    End Sub

If I just Return Concept, however, It only gives me 1 OBS. Same for Return Number.
 
After adding a few more MsgBox's I found that it was doing both the If Then and the Else back to back.

I just pulled the whole If statement out and am just running this instead.

VB.NET:
    Private Function GetCombinedText() As String
        Dim Concept As String
        Dim Number As String
        Concept = CStr(Me.ComboBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
        Number = CStr(Me.TextBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
        Dim store As String = Concept & Number
        Return store
    End Function

With this I get a StackOverflowException with the details only saying "Property evaluation failed" at the first invoke.
 
OOOooookay.

Because I was getting either doubled up or errors results I went ahead and split the thing into 2 separate functions. I don't know how this impacts performance but it's working.

currently rocking:

VB.NET:
Private Delegate Function GetCombinedTextInvoker() As String
With
VB.NET:
    Private Function GetNumberText() As String
        Dim number As String
        If Me.TextBox1.InvokeRequired Then
            number = CStr(Me.TextBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetNumberText)))
        Else
            number = TextBox1.Text
        End If
        Return number
    End Function

    Private Function GetConceptText() As String
        Dim Concept As String
        If Me.ComboBox1.InvokeRequired Then
            Concept = CStr(Me.ComboBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetConceptText)))
        Else
            Concept = ComboBox1.Text
        End If

        Return Concept
    End Function

And then I just call it in the background worker with
VB.NET:
        Dim store As String = GetConceptText() & GetNumberText()
 
You're obviously not understanding what's happening so let's go into a bit more detail, using your code form post #6 to demonstrate. Here is your code:
    Private Function GetCombinedText() As String
        Dim Concept As String
        Dim Number As String
        If Me.ComboBox1.InvokeRequired And Me.TextBox1.InvokeRequired Then
            Concept = CStr(Me.ComboBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
            Number = CStr(Me.TextBox1.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
        Else
            Concept = Me.ComboBox1.Text
            Number = Me.TextBox1.Text
        End If

        Return Concept & Number

    End Function
Now, what is supposed to be happening is that you write a method that will first test whether it is executing on the thread that owns the control(s) it wants to access. That's what InvokeRequired is for. It will return True if you are required to use an invocation to access the control. Now, your TextBox and ComboBox are obviously owned by the same thread so there's no point testing the InvokeRequired properties of both. You should just pick one arbitrarily. Alternatively, when I need to access more than one control, I tend to use the form itself. Obviously the thread that owns a control must also own the form its on, so that will produce the same result. It also seems "more correct" when accessing multiple controls on that same form. With in mind, here's the first change:
    Private Function GetCombinedText() As String
        Dim Concept As String
        Dim Number As String
        If Me.InvokeRequired Then
            Concept = CStr(Me.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
            Number = CStr(Me.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
        Else
            Concept = Me.ComboBox1.Text
            Number = Me.TextBox1.Text
        End If

        Return Concept & Number

    End Function
Now, what is that If...Else all about? The idea is that if we are not on the correct thread we make an invocation, i.e. we use a delegate to cross the thread boundary to the thread that owns the control(s) and then invoke a method. That's what the Invoke method is for. It says "create a delegate that refers to some method and invoke it on my owning thread". The method we are invoking is the same method we are currently in. That's why the If and the Else get executed. You call the method on the background thread and it enters the If block. That will call invoke and it will execute the same method a second time, this time on the UI thread. This time the Else block will get executed, which should access the control(s) directly. The data retrieved from the controls is then returned, which gets passed back across the thread boundary into the If block of the first call and there it gets used.

So, what's wrong with your code? It enters the If block and gets to this line:
Concept = CStr(Me.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
What does that line do? It crosses the thread boundary and invokes the same method again on the UI thread. What does that call do? It enters the Else block, gets the Text from both controls, combines them and return the result. That combined result then gets assigned to the Concept variable. You now setps to the next line and do basically the same thing again, assigning the combined result to the Number variable. You now have two variables that both contain the combined result and you combine them, which is why you get duplication.

You should only be calling Invoke once. You only need to cross the thread boundary once. While you're there, do everything you need to do in one go. So, your If block should call Invoke once while your Else block should get both values and combine them:
    Private Function GetCombinedText() As String
        Dim combinedText As String

        If Me.InvokeRequired Then
            combinedText = CStr(Me.Invoke(New GetCombinedTextInvoker(AddressOf GetCombinedText)))
        Else
            combinedText = Me.ComboBox1.Text & Me.TextBox1.Text
        End If

        Return combinedText
    End Function
 
I find it most strange that BackgroundWorker was devised to make all this Invoke stuff easy, but it's being completely ignored here..

Do whatever work NOT INVOLING UI COMPONENTS in your backgroundworker's doWork(), set the BGW's WorkerReportsProgress property to true, create a method that Handles the ProgressChanged event, and in that method body do all your UI work:
VB.NET:
Sub Whatever(i as Int, o as Object) Handles bgw.ProgressChanged
 myProgressBar.Value = i

 myStatusArea.Text = o.ToString()
End Sub

and every time you want to update the UI, inside the doWork() event, say bgw.ReportProgress() and pass some sensible values in (note, you cannot access the UI components inside doWork()!! if you have to access UI components, do it inside the sub that handles the progress event

VB.NET:
'now i'm 50% done and onto the Phase 4, will uipdate the UI
bgw.ReportProgress(50, "Phase 4")

'some more code
....

'now i'm 75% done and onto the Phase 5, will uipdate the UI
bgw.ReportProgress(75, "Phase 5")

There's no requirement for the progrss int to climb all the time, i've used it to signal phases or controls to update before:

VB.NET:
Sub Whatever(i as Int) Handles bgw.ProgressChanged
 If i = 0 Then
  label1.Text = "Phase 1 done"
 Else If i = 1 Then
  textBox1.Text = "Phase 2 done"
 ...
 
Note if you want arguments from ui components to be provided to your backgroundworker, that's what the RunWorkerAsync(object) method is for- whatever you pass as object is passed to doWork() as an argument

If youre wondering why you cannot easily see a mechanism to continuously and at random retrieve data from the ui while a BGW is DoWork()ing it's because it doesnt make sense to do - the whole idea of doing it in the background is that you get your necessary data at the start and then run the op in the background without further user interaction. If an operation takes 4 phases and the user must do something at each phase, then wire it upo to the UI like that ; 4 calls to DoWork with different arguments..
 
Back
Top