Link to home
Start Free TrialLog in
Avatar of mcguirestick
mcguirestick

asked on

VB 2005 BackgroundWorker Problem passing values to commnad buttons

I know there are a few threads regarding this matter but i still can not get my head round this.
I have a windows form with a number of Command Buttons on. Every so ofter a timer fires which opens a spreadsheet and updates the Caption and backColour of these buttons. When the timer fires it starts the thread (BackgroundWorker) and all runs fine till i try to change the caption on the button the program then crashes. As i am updating 18 button concesotivly i realise you need to pass the result back but can not figure how. I have seen one solution that switches threads but sort of defeats the point of running in a seperate thread in the first place.
Here is a bit of the code
 Public Sub FleetCheck()

        Dim Line As Integer
        Dim SheetNo As Integer
        Dim TempStr As String
        Dim TempStr2 As String
        Dim Found As Boolean
        Dim I, A, B As Integer
        Dim SheetTemp As Integer
        Dim Dates, Times As String

        Dates = Mid(Now, 1, 10)
        Times = Mid(Now, 12, 5)
        xlsApp.ActiveWorkbook.RefreshAll()

        ' check no of rows on spread sheet

        Line = 4
        Do While Found = False
            TempStr = xlsSheet.Range("A" & Line).Value
            Line = Line + 1
            If TempStr = "" Then Found = True
        Loop
        SheetNo = Line
        'MsgBox("Found it " & SheetNo)

        Found = False
        Line = 0
        Do While Found = False
            If CallSignArray(Line) = "" Then Found = True
            Line = Line + 1
        Loop
        ArrayNo = Line - 2
        'MsgBox(ArrayNo)

        ReDim FleetArray(ArrayNo, 7)

        B = 0
        SheetTemp = SheetNo + 4
        For A = 0 To ArrayNo
            For I = 4 To SheetTemp
                TempStr = CallSignArray(A)
                TempStr2 = xlsSheet.Range("D" & I).Value
                If TempStr = TempStr2 Then
                    FleetArray(B, 0) = xlsSheet.Range("D" & I).Value 'Callsign
                    FleetArray(B, 1) = xlsSheet.Range("A" & I).Value ' Fleet
                    FleetArray(B, 2) = xlsSheet.Range("E" & I).Value 'CAD
                    FleetArray(B, 3) = xlsSheet.Range("F" & I).Value 'Status
                    FleetArray(B, 4) = xlsSheet.Range("H" & I).Value 'Mins
                    FleetArray(B, 5) = xlsSheet.Range("I" & I).Value & xlsSheet.Range("J" & I).Value 'Map
                    FleetArray(B, 6) = xlsSheet.Range("L" & I).Value 'Speed
                    FleetArray(B, 7) = xlsSheet.Range("N" & I).Value 'Location
                    B = B + 1
                End If
            Next
        Next
        'Fill Call Signs labels
        A = 0
        If FleetArray(A, 1) <> "" Then
           

            FrmMain.Cmd1.Text = FleetArray(A, 0) & " " & FleetArray(A, 4)
            If FleetArray(A, 3) = "grn at stn" Then FrmMain.Cmd1.BackColor = Color.GreenYellow
         
            A = A + 1
        End If

        If FleetArray(A, 1) <> "" Then
            FrmMain.Cmd2.Text = FleetArray(A, 0) & " " & FleetArray(A, 4) ' ******** Program crashes here ****
            If FleetArray(A, 3) = "grn at stn" Then FrmMain.Cmd2.BackColor = Color.GreenYellow
                       A = A + 1
        End If

        If FleetArray(A, 1) <> "" Then
            FrmMain.Cmd3.Text = FleetArray(A, 0) & " " & FleetArray(A, 4)
            If FleetArray(A, 3) = "grn at stn" Then FrmMain.Cmd3.BackColor = Color.GreenYellow
                        A = A + 1
        End If

etc etc etc

Any help would be appreciated
Avatar of AkisC
AkisC
Flag of Greece image

The problem you face is that when a variable is created in a thread other that the main thread (e.g. Form1) can not update the main thread.

1.- So the first thing to do is create a Public Delegate sub
Public Delegate Sub printString_ToMainThread(ByVal vBuffer As String)

2.- Let's say you have a textbox named txtReadingBufferNow

3.- Now on the BackgroundWorker thread you get the values you want (e.g. aCHR=somthing you want)
and you want to update the txtReadingBufferNow.text=aCHR
You call
txtReadingBufferNow.BeginInvoke(New printString_ToMainThread(AddressOf ReadingNow_Display), aCHR)

4.- ReadingNow_Display is a private or public sub that updates the main thread
    Private Sub ReadingNow_Display(ByVal vTXT As String)
        Me.txtReadingBufferNow.Text = vTXT
        Me.txtReadingBufferNow.Refresh()
    End Sub

I hope it helps
Hi mcguirestick;

I am assuming that the code above is in the BackgroundWorker thread. It is not allowed to modify the controls of a GUI from another thread, which is known as a cross thread error.

In the BackgroundWorker model there are two methods that you can place code to modify the controls of a GUI. One is the ProgressChanged method and the other is RunWorkerCompleted method and this is because the system make a call to the GUI thread to make the change.

If you do not want to use one of the above methods then you will have to do something like this.

    ' At class level define delegate to be called from another thread. In this example I am updating a
    ' Ritch text box.
    Private Delegate Sub rtbUpdateDelegate(ByVal msg As String)
   
    ' From within the thread code to update a GUI define a variable with the delegate
    Dim rtbSafeUpdate As New rtbUpdateDelegate(AddressOf UpdateTargetRtb)    
    '....
    ' tcOutput is a TabControl on a GUI which has the RichTextbox and rtbSafeUpdate is the delegate that
    ' will do the update for the thread. Also arg is a string to be sent to the RichTextBox.
    tcOutput.Invoke(rtbSafeUpdate, arg)
   
    ' The delegated method to do the acutual update
    Private Sub UpdateTargetRtb(ByVal msg As String)

        ' This function is called from another thread to update the
        ' RichTextBox for Replace with matches
        rtbReplaceResults.Text = msg

    End Sub

Fernando
Avatar of mcguirestick
mcguirestick

ASKER

I will give these two ago tomorrow. FernandoSoto: mentions using process changed method is that more complicated to use as i am a novice to VB 2005 but i think i do understand the first example. I will let you all know tomorrow
Thanks Terry
Hi mcguirestick;

Using the process changed method is much easier then the other way. Below is sample code of a BackgroundWorker thread that makes a call to the BackgroundWorker.ReportProgress which raises the event ProgressChanged where you can direct interact with the GUI without cross thread violations. This sample code does a file search recursively and interfaces directly with the GUI. Note that the ProgressChanged event does not have to just report changes but can be used to changes the values of the forms controls.

This code sample assumes that the form has the following controls on it:
TextBox called txtSearchPath
TextBox called txtSearchPattern
Label   called lblMessage
Button  called btnStart
ListBox called lstFiles


Imports System.IO

Public Class SearchFilesFrm

    ' To set up for a background operation, add an event handler for the DoWork event.
    ' Call your time-consuming operation in this event handler. To start the operation,
    ' call RunWorkerAsync. To receive notifications of progress updates, handle the
    ' ProgressChanged event. To receive a notification when the operation is completed,
    ' handle the RunWorkerCompleted event.
    ' Note  
    ' 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.
    ' If your background operation requires a parameter, call RunWorkerAsync with your
    ' parameter. Inside the DoWork event handler, you can extract the parameter from
    ' the DoWorkEventArgs.Argument property.

    ' Store the results of the search
    Private Files As List(Of String)
    ' Used to prevent nested calls to ProgressChanged
    Private CallInProgress As Boolean
    ' The Worker thread
    Private WithEvents BgWorker As New System.ComponentModel.BackgroundWorker

    Private Sub SearchFilesFrm_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load

        ' Enable ProgressChanged and Cancellation mothed
        BgWorker.WorkerReportsProgress = True
        BgWorker.WorkerSupportsCancellation = True

    End Sub

    ' Start and Stop the search
    Private Sub btnStart_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles btnStart.Click

        If btnStart.Text = "Start" Then
            If Not Directory.Exists(txtSearchPath.Text.Trim()) Then
                MessageBox.Show("The Search Directory does not exist", _
                    "Path Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
                Return
            End If
            If txtSearchPattern.Text.Trim() = "" Then txtSearchPattern.Text = "*"
            Dim arguments() As String = {txtSearchPath.Text.Trim(), _
                txtSearchPattern.Text.Trim()}

            lstFiles.Items.Clear()
            ' Start the background worker thread. Thread runs in DoWork event.
            BgWorker.RunWorkerAsync(arguments)
            btnStart.Text = "Stop"
        Else
            BgWorker.CancelAsync()
        End If

    End Sub

    ' Start the Asynchronous file search. The background thread does it work from
    ' this event.
    Private Sub BgWorker_DoWork(ByVal sender As Object, _
        ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BgWorker.DoWork

        'Retrieve the search path which was requested
        Dim path As String = DirectCast(e.Argument, String())(0)
        Dim pattern As String = DirectCast(e.Argument, String())(1)

        ' Invoke the worker procedure
        Files = New List(Of String)
        SearchFiles(path, pattern)
        ' Return a result to the RunWorkerCompleted event
        Dim message As String = String.Format("Found {0} Files", Files.Count)
        e.Result = message

    End Sub

    ' Recursively search directory and sub directories
    Private Sub SearchFiles(ByVal path As String, ByVal pattern As String)

        ' Displat message
        Dim message As String = String.Format("Parsing Directory {0}", path)
        BgWorker.ReportProgress(0, message)

        'Read the files and if the Stop button is pressed cancel the operation
        For Each fileName As String In Directory.GetFiles(path, pattern)
            If BgWorker.CancellationPending Then Return
            Files.Add(fileName)
        Next
        For Each dirName As String In Directory.GetDirectories(path)
            If BgWorker.CancellationPending Then Return
            SearchFiles(dirName, pattern)
        Next

    End Sub

    ' The bacground thread calls this event when you make a call to ReportProgress
    ' It is OK to access user controls in the UI from this event.
    ' If you use a Progress Bar or some other control to report the tasks progress
    ' you should avoid unnecessary calls to ReportProgress method because this causes
    ' a thread switch which is a relatively expensive in terms of processing time.
    Private Sub BgWorker_ProgressChanged(ByVal sender As Object, _
        ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
        Handles BgWorker.ProgressChanged

        ' Reject a nested call.
        If CallInProgress Then Return
        CallInProgress = True
        ' Display the message received in the UserState property
        lblMessage.Text = e.UserState.ToString()
        ' Display all files added since last call.
        For idx As Integer = lstFiles.Items.Count To Files.Count - 1
            lstFiles.Items.Add(Files(idx))
        Next
        ' If a Me.Refresh is in this code you will need to place a Application.DoEvents()
        ' otherwise the UI will not respond without them it works fine.
        ' Me.Refresh()
        ' Let the Windows OS process messages in the queue
        ' Application.DoEvents()
        CallInProgress = False

    End Sub

    ' The background thread calls this event just before it reaches the End Sub
    ' of the DoWork event. It is OK to access user controls in the UI from this event.
    Private Sub BgWorker_RunWorkerCompleted(ByVal sender As Object, _
        ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
        Handles BgWorker.RunWorkerCompleted

        ' Display the last message and reset the Start/Stop button text
        lblMessage.Text = e.Result.ToString()
        btnStart.Text = "Start"

    End Sub

End Class


Fernando
Hi,
I have tried both the diffrent suggestions and i am getting the same error on both.
The error is generated on this line
tcOutput.Invoke(rtbSafeUpdate, arg)
The error is:-
An error occurred creating the form. See Exception.InnerException for details.  The error is: ActiveX control '926a6756-7fc7-4427-bff2-cd69fa9f4f62' cannot be instantiated because the current thread is not in a single-threaded apartment.
Any
Can you post you code as is now?
I have command buttons on a form called  Cmd1, Cmd2, Cmd3 etc on a frame called Frame3 on a form called FrmMain.
On pressing a command button this code gets fired:-
Private Sub Button13_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button13.Click
        'Thread Code
        BW1.RunWorkerAsync()
    End Sub
This fires:-
Private Sub BW1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BW1.DoWork
        FleetCheck()
        BW1.CancelAsync()
    End Sub
Which Fires a Module Called FleetCheck:-
 Public Sub FleetCheck()

        Dim Line As Integer
        Dim SheetNo As Integer
        Dim TempStr As String
        Dim TempStr2 As String
        Dim Found As Boolean
        Dim I, A, B As Integer
        Dim SheetTemp As Integer
        Dim Dates, Times As String

        Dim rtbSafeUpdate As New rtbUpdateDelegate(AddressOf UpdateTargetRtb)

        Dates = Mid(Now, 1, 10)
        Times = Mid(Now, 12, 5)
        xlsApp.ActiveWorkbook.RefreshAll()

        ' check no of rows on spread sheet

        Line = 4
        Do While Found = False
            TempStr = xlsSheet.Range("A" & Line).Value
            Line = Line + 1
            If TempStr = "" Then Found = True
        Loop
        SheetNo = Line
        'MsgBox("Found it " & SheetNo)

        Found = False
        Line = 0
        Do While Found = False
            If CallSignArray(Line) = "" Then Found = True
            Line = Line + 1
        Loop
        ArrayNo = Line - 2
        'MsgBox(ArrayNo)

        ReDim FleetArray(ArrayNo, 7)

        B = 0
        SheetTemp = SheetNo + 4
        For A = 0 To ArrayNo
            For I = 4 To SheetTemp
                TempStr = CallSignArray(A)
                TempStr2 = xlsSheet.Range("D" & I).Value
                If TempStr = TempStr2 Then
                    FleetArray(B, 0) = xlsSheet.Range("D" & I).Value 'Callsign
                    FleetArray(B, 1) = xlsSheet.Range("A" & I).Value ' Fleet
                    FleetArray(B, 2) = xlsSheet.Range("E" & I).Value 'CAD
                    FleetArray(B, 3) = xlsSheet.Range("F" & I).Value 'Status
                    FleetArray(B, 4) = xlsSheet.Range("H" & I).Value 'Mins
                    FleetArray(B, 5) = xlsSheet.Range("I" & I).Value & xlsSheet.Range("J" & I).Value 'Map
                    FleetArray(B, 6) = xlsSheet.Range("L" & I).Value 'Speed
                    FleetArray(B, 7) = xlsSheet.Range("N" & I).Value 'Location
                    B = B + 1
                End If
            Next
        Next
        'Fill listview headers

        FrmMain.ListView2.Clear()

        FrmMain.ListView2.View = View.Details
        FrmMain.ListView2.Columns.Add("CallSign", 65, HorizontalAlignment.Left)
        FrmMain.ListView2.Columns.Add("FleetNo", 65, HorizontalAlignment.Left)
        FrmMain.ListView2.Columns.Add("CAD", 50, HorizontalAlignment.Left)
        FrmMain.ListView2.Columns.Add("Status", 175, HorizontalAlignment.Left)
        FrmMain.ListView2.Columns.Add("Mins", 50, HorizontalAlignment.Left)
        FrmMain.ListView2.Columns.Add("MapRef", 105, HorizontalAlignment.Left)
        FrmMain.ListView2.Columns.Add("Speed", 65, HorizontalAlignment.Left)
        FrmMain.ListView2.Columns.Add("Location", 175, HorizontalAlignment.Left)
        A = 0
        If FleetArray(A, 1) <> "" Then
            'MsgBox(FleetArray(A, 0))
            'MsgBox(FleetArray(A, 4))
************** CRASHES HERE *******************************************************************
            FrmMain.Frame3.Invoke(rtbSafeUpdate, "Hello")

               FrmMain.Cmd1.Text = FleetArray(A, 0) & " " & FleetArray(A, 4)
            If FleetArray(A, 3) = "grn at stn" Then FrmMain.Cmd1.BackColor = Color.GreenYellow
            If FleetArray(A, 3) = "grn away from veh" Then FrmMain.Cmd1.BackColor = Color.Green
            If FleetArray(A, 3) = "red at hosp" Then FrmMain.Cmd1.BackColor = Color.Red
            If FleetArray(A, 3) = "red at scn" Then FrmMain.Cmd1.BackColor = Color.DeepPink
            If FleetArray(A, 3) = "red to hosp" Then FrmMain.Cmd1.BackColor = Color.IndianRed
            If FleetArray(A, 3) = "grn at stn" Then FrmMain.Cmd1.BackColor = Color.GreenYellow
            If FleetArray(A, 3) = "unavailable" Then FrmMain.Cmd1.BackColor = Color.Pink
            If FleetArray(A, 3) = "amb to scn" Then FrmMain.Cmd1.BackColor = Color.Orange
            If FleetArray(A, 3) = "grn at sby" Then FrmMain.Cmd1.BackColor = Color.LimeGreen
            If FleetArray(A, 3) = "green mob" Then FrmMain.Cmd1.BackColor = Color.LightGreen
            If FleetArray(A, 3) = "uninterruptible rest break" Then FrmMain.Cmd1.BackColor = Color.DodgerBlue

            A = A + 1
        End If

              For A = 0 To ArrayNo
            'FrmMain.ListBox1.Items.Add(FleetArray(A, 0) & " - " & FleetArray(A, 1) & " - " & FleetArray(A, 2) & " - " & FleetArray(A, 3) & " - " & FleetArray(A, 4))

            If FleetArray(A, 0) = "" Then Exit For

            lSingleItem = FrmMain.ListView2.Items.Add(FleetArray(A, 0))

            lSingleItem.SubItems.Add(FleetArray(A, 1))
                Next
        FrmMain.Frame3.Caption = "Vehicle Status on " & Dates & " at " & Times
    End Sub

Finnally:-
Private Delegate Sub rtbUpdateDelegate(ByVal msg As String)
    Private Sub UpdateTargetRtb(ByVal msg As String)

        ' This function is called from another thread to update the
        ' RichTextBox for Replace with matches
        FrmMain.Cmd1.Text = msg

    End Sub
ASKER CERTIFIED SOLUTION
Avatar of Fernando Soto
Fernando Soto
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Hi,
Thanks for all your work.
I did it a diffrent way in the end.
I basically populated the array with all the fields uring the new thread and then just read back the array and populated the command buttons using the Array. Its not perfect but so much faster than it was before.
Thanks
Terry
Not a problem, glad you got it working. ;=)