Threading problem

TyB

Well-known member
Joined
May 14, 2009
Messages
102
Programming Experience
3-5
I have a datagridview control on my form that is used to display data from a DB query.

I am using a backgoundworker control that is used to get the data so I can display a progress bar for user notification. When the thread completes I rebind the data source of the grid and show the data.

This all works great the first time, but if the user runs another query I get a cross threading error on the grid.

Not sure how to get around this as the thread completed so it should be released. The grid is created in the main form thread then marked as visible by the backgroundworker.

Any incite would be appreciated.

Ty
 
You should not be doing anything related to the UI in the DoWork event handler. Anything that updates the UI should be done in the ProgressChanged or RunWorkerCompleted event handler.
 
It is in the RunWorkerComplete sub. That is why I am confused as to why it throws an error. I do not have a progress changed sub as the progress bar is just an animation that is shown before the thread is created.

Ty
 
Code

Here is the code. This first block is a button click that starts the process.
VB.NET:
    Private Sub btnsearch_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnsearch.Click
        'Create a new thread that will be used to get the data from the DB.
        'Show the please wait image

        Panel3.Visible = True

        dtDate = DateTimePicker1.Value

        'Because we are working with threads we will create the datagridview controls dynamically so we do not get any 
        'cross threading issues.

        'Set the dimensions
        dgvw.Width = 932
        dgvw.Height = 430

        dgvw.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill 'forces the grid to be the full width of the control

        Panel4.Controls.Add(dgvw)

        BackgroundWorker1.RunWorkerAsync(2000)

    End Sub

Here is the DoWork sub of the background thread.
VB.NET:
    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

        ' Do not access the form's BackgroundWorker reference directly.
        ' Instead, use the reference provided by the sender parameter.
        Dim bw As BackgroundWorker = CType(sender, BackgroundWorker)

        ' Extract the argument.
        Dim arg As Integer = Fix(e.Argument)

        ' Start the time-consuming operation.
        e.Result = FillGrid(bw, arg)

        ' If the operation was canceled by the user, 
        ' set the DoWorkEventArgs.Cancel property to true.
        If bw.CancellationPending Then
            e.Cancel = True
        End If

    End Sub

This is the process that is done by the thread.
VB.NET:
    Public Function FillGrid(ByVal bw As BackgroundWorker, ByVal sleepPeriod As Integer) As Boolean
        'Fill the table with the items that expire with in the month and year the user picks in the datetimepicker
        Dim dtExpired As Date
        Dim dtbegin As Date
        Dim dtend As Date
        Dim strfilepath As String

        While Not bw.CancellationPending
            Dim [exit] As Boolean = False
            Try
                Dim path As String
                path = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase)
                'Extract the path returned minus the file:\ that is placed at the begining.
                path = Mid(path, 7)

                If My.Computer.FileSystem.FileExists(path & "\Source.txt") Then

                    ' Add code to read text from a file.
                    ' Create an instance of StreamReader to read from a file.
                    Using sr As StreamReader = New StreamReader("Source.txt")
                        Dim line As String
                        ' Read and display the lines from the file until the end 
                        ' of the file is reached.
                        'Do
                        line = sr.ReadLine()
                        strfilepath = line
                        'Loop Until line Is Nothing
                    End Using


                    'Get the date the user picked.
                    dtExpired = dtDate

                    'Now get the month and year to be used as the begin date
                    dtbegin = dtExpired.Month & "/1/" & dtExpired.Year

                    dtend = dtbegin.AddMonths(1)

                    cn = New OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Temp Projects\PSNA Application\PSNA2\PSNA\PSNA\bin\PSNA Application Expiration.accdb;") '" & strfilepath & ";")
                    'provider to be used when working with access database
                    cn.Open()
                    cmd = New OleDbCommand("SELECT [Program Title], [Contact Hours], [Start Date], [Expiration Date], Presenter, [Type of Program], Method FROM Programs WHERE ([Expiration Date] BETWEEN @date1 AND @date2)", cn)

                    cmd.Parameters.Add(New OleDbParameter("@date1", OleDbType.Date)).Value = dtbegin
                    cmd.Parameters.Add(New OleDbParameter("@date2", OleDbType.Date)).Value = dtend
                    dr = cmd.ExecuteReader

                    If dr.HasRows = True Then
                        'Add the return data as a new source to the binding source
                        bs.DataSource = dr
                    Else
                        MsgBox("There are no records that match your criteria.", MsgBoxStyle.OkOnly, "No Matches")
                    End If

                    dr.Close()
                    cn.Close()

                Else
                    MsgBox("The file Source.txt is missing.", MsgBoxStyle.OkOnly, "File Missing")
                End If
            Catch ex As Exception
                Dim strerror As String
                strerror = ex.Message.ToString

                MsgBox(ex.Message.ToString, MsgBoxStyle.OkOnly, "Error")

                System.Diagnostics.Debug.WriteLine(strerror)
            End Try

            [exit] = True

            If [exit] Then
                Exit While
            End If

        End While

        Return True
    End Function

And finally the RunWorkerComplete sub that is the only place that the UI is manipulated.
VB.NET:
    Private Sub backgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted

        If e.Cancelled Then
            ' The user canceled the operation.
            MessageBox.Show("Operation was canceled", "Proccess cancelled", MessageBoxButtons.OK)
        ElseIf (e.Error IsNot Nothing) Then
            ' There was an error during the operation.
            Dim msg As String = String.Format("An error occurred: {0}", e.Error.Message)
            MessageBox.Show(msg)
        Else
            'Hide the please wait panel and fill the datagrid
            Panel3.Visible = False
            dgvw.DataSource = bs
        End If

    End Sub

This all works great on the first button click. On the second button click I get the following error. "Cross Thread operation not valid:Control "dgvw" accessed from a thread other that the one it was created on."

Thanks Ty
 
You're binding to the BindingSource on the secondary thread. Binding is a UI-related operation. No UI-related operations on any thread other than the UI thread. On the first run through the BindingSource isn't bound to the grid. On the second run through it is, hence the cross-thread error on the second run through.
 
The binding source and the grid are created at a module level variable so should be on the main thread. I'm not sure I understand how to get around this. Panel3 is also on the main thread and it does not trow the error on the second run so it does seem logical that it is the binding source but since they are all on the main thread and the background complete sub is where you interact with the UI.

Ty
 
The grid was created on the UI thread, of course. The BindingSource is bound to the grid. If you set the DataSource of the BindingSource and that BindingSource is bound to the grid then logic would dictate that the grid is going to be updated. If you set the DataSource of the BindingSource on the secondary thread then logic would dictate that the grid is going to be updated on the secondary thread, which is exactly the problem. Do NOT bind to the BindingSource anywhere but the RunWorkerCompleted event handler.
 
I did see that I was missing that the binding source datatsource was set in the second thread after I posted my last message.

If I move that to the runcomplete any thoughts on how to assign it to the datareader that is returned in the fillgrid method that will not cause cross threading?

Thanks,

Ty
 
First up, your FillGrid method needs a name change because it's not filling the grid. That method should return the new data source, which you can then assign to e.Result and then use in the RunWorkerCompleted event handler. You should use your DataReader to populate a DataTable and use that as the data source.
 
Got it

jmcilhinney,
Once again you have helped me see the light. I was so focused on the problem that I could not see the forest for the trees as it were. I had completly forgot about the e.results.

Here is the code for anyone that wants to add threading to a project. Hope it helps and the learning continues.

Start the threading process with a button click.
VB.NET:
    Private Sub btnsearch_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnsearch.Click
        'Create a new thread that will be used to get the data from the DB.
        'Show the please wait image

        Panel3.Visible = True

        dtDate = DateTimePicker1.Value

        BackgroundWorker1.RunWorkerAsync(2000)

    End Sub

VB.NET:
    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        ' Do not access the form's BackgroundWorker reference directly.
        ' Instead, use the reference provided by the sender parameter.
        Dim bw As BackgroundWorker = CType(sender, BackgroundWorker)

        ' Extract the argument.
        Dim arg As Integer = Fix(e.Argument)

        'Panel3.Visible = True

        ' Start the time-consuming operation.
        e.Result = GetData(bw, arg)

        ' If the operation was canceled by the user, 
        ' set the DoWorkEventArgs.Cancel property to true.
        If bw.CancellationPending Then
            e.Cancel = True
        End If

    End Sub

VB.NET:
    Public Function GetData(ByVal bw As BackgroundWorker, ByVal sleepPeriod As Integer) As DataTable
        'Fill the table with the items that expire with in the month and year the user picks in the datetimepicker
        Dim dtExpired As Date
        Dim dtbegin As Date
        Dim dtend As Date
        Dim strfilepath As String
        Dim dt As New DataTable("Program Expiration")
        Dim TempRow As DataRow


        While Not bw.CancellationPending
            Dim [exit] As Boolean = False
            Try
                'Set up the datatable that will be used as the return datasource for the datagridview
                Dim PT As DataColumn = New DataColumn("Program Title")
                'declaring a column name
                PT.DataType = System.Type.GetType("System.String")
                'setting the datatype for the column
                dt.Columns.Add(PT)
                'adding the column to table
                Dim CH As DataColumn = New DataColumn("Contact Hours")
                CH.DataType = System.Type.GetType("System.String")
                dt.Columns.Add(CH)

                Dim SD As DataColumn = New DataColumn("Start Date")
                SD.DataType = System.Type.GetType("System.String")
                dt.Columns.Add(SD)

                Dim ED As DataColumn = New DataColumn("Expiration Date")
                ED.DataType = System.Type.GetType("System.String")
                dt.Columns.Add(ED)

                Dim Presenter As DataColumn = New DataColumn("Presenter")
                Presenter.DataType = System.Type.GetType("System.String")
                dt.Columns.Add(Presenter)

                Dim TP As DataColumn = New DataColumn("Type of Program")
                TP.DataType = System.Type.GetType("System.String")
                dt.Columns.Add(TP)

                Dim Method As DataColumn = New DataColumn("Method")
                Method.DataType = System.Type.GetType("System.String")
                dt.Columns.Add(Method)

                Dim path As String
                path = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase)
                'Extract the path returned minus the file:\ that is placed at the begining.
                path = Mid(path, 7)

                If My.Computer.FileSystem.FileExists(path & "\Source.txt") Then

                    ' Add code to read text from a file.
                    ' Create an instance of StreamReader to read from a file.
                    Using sr As StreamReader = New StreamReader("Source.txt")
                        Dim line As String
                        ' Read and display the lines from the file until the end 
                        ' of the file is reached.
                        'Do
                        line = sr.ReadLine()
                        strfilepath = line
                        'Loop Until line Is Nothing
                    End Using

                    'Get the date the user picked.
                    dtExpired = dtDate

                    'Now get the month and year to be used as the begin date
                    dtbegin = dtExpired.Month & "/1/" & dtExpired.Year

                    dtend = dtbegin.AddMonths(1)

                    cn = New OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Temp Projects\PSNA Application\PSNA2\PSNA\PSNA\bin\PSNA Application Expiration.accdb;") '" & strfilepath & ";")
                    'provider to be used when working with access database
                    cn.Open()
                    cmd = New OleDbCommand("SELECT [Program Title], [Contact Hours], [Start Date], [Expiration Date], Presenter, [Type of Program], Method FROM Programs WHERE ([Expiration Date] BETWEEN @date1 AND @date2)", cn)

                    cmd.Parameters.Add(New OleDbParameter("@date1", OleDbType.Date)).Value = dtbegin
                    cmd.Parameters.Add(New OleDbParameter("@date2", OleDbType.Date)).Value = dtend
                    dr = cmd.ExecuteReader

                    If dr.HasRows = True Then
                        Do While dr.Read
                            'Fill the datatable
                            TempRow = dt.NewRow
                            TempRow.Item(0) = dr(0)
                            TempRow.Item(1) = dr(1)
                            TempRow.Item(2) = dr(2)
                            TempRow.Item(3) = dr(3)
                            TempRow.Item(4) = dr(4)
                            TempRow.Item(5) = dr(5)
                            TempRow.Item(6) = dr(6)

                            dt.Rows.Add(TempRow)
                        Loop

                    Else
                        MsgBox("There are no records that match your criteria.", MsgBoxStyle.OkOnly, "No Matches")
                    End If

                    dr.Close()
                    cn.Close()

                Else
                    MsgBox("The file Source.txt is missing.", MsgBoxStyle.OkOnly, "File Missing")
                End If
            Catch ex As Exception
                Dim strerror As String
                strerror = ex.Message.ToString

                MsgBox(ex.Message.ToString, MsgBoxStyle.OkOnly, "Error")

                System.Diagnostics.Debug.WriteLine(strerror)
            End Try

            [exit] = True

            If [exit] Then
                Exit While
            End If

        End While

        Return dt
    End Function

VB.NET:
    Private Sub backgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted

        If e.Cancelled Then
            ' The user canceled the operation.
            MessageBox.Show("Operation was canceled", "Proccess cancelled", MessageBoxButtons.OK)
        ElseIf (e.Error IsNot Nothing) Then
            ' There was an error during the operation.
            Dim msg As String = String.Format("An error occurred: {0}", e.Error.Message)
            MessageBox.Show(msg)
        Else
            'Hide the please wait panel and fill the datagrid
            bs.DataSource = e.Result
            dgvw.DataSource = bs
            Panel3.Visible = False
        End If

    End Sub

Thanks again for your help.

Ty
 
Back
Top