Search code examples
objectlistview

Events causing cross-thread error in backgroundworker_progresschanged and backgroundworker_complete


My VB.NET winforms app runs a timer which creates a background worker to update the objects in an ObjectListView.

In the timer loop, a number of 'device' objects are added to an observable collection (in the backgroundworker_progresschanged event) and (in the backgroundworker_complete event), I use an OLV.SetObjects(allDevices, true) to populate them.

This all works flawlessly. However, the currently selected items in the OLV are lost during the OLV.setobjects so I need to restore them.

To do this, (in the backgroundworker_complete event), I want to access the selecteditems property of the OLV but I keep getting a "Cross-thread operation not valid: Control 'DeviceListView1' accessed from a thread other than the thread it was created on." All attempts at trying to read the selected listviewitems (either by OLV.selecteditems or a loop reading them from the OLV) fail with the cross-thread exception.

I may misunderstand but I thought I could access GUI elements on the backgroundworker_progresschanged and backgroundworker_complete events?

Here's the relevant code:

The PopulateDevices sub is called when the timer is started and will not run again until a specific time has passed. It runs the RunWorkerAsync of the Worker.

    Public Sub PopulateDevices()

        ' Debug
        _UpdateCount += 1

        ' Pause the Update Timer
        UpdateTimer.Stop()

        ' Get the Starting Time of this Update
        StartTime = DateTime.Now

        ' Stop updating the DeviceListView1 ObjectListView
        ControlHelper.ControlInvoke(DeviceListView1, Sub() DeviceListView1.BeginUpdate())

        ' Clear Existing Devices from the List
        AllDevices = New TrulyObservableCollection(Of DeviceItem)

        ' Get the selected devices
        '_SelectedDevices = GetSetSelectedDevices(DeviceListView1)

        ' Prep the BackgroundWorker
        PopulateDevicesWorker = New BackgroundWorker
        PopulateDevicesWorker.WorkerReportsProgress = True

        ' Add the Event Handlers
        AddHandler PopulateDevicesWorker.DoWork, AddressOf PopulateDevicesWorkerDoWork
        AddHandler PopulateDevicesWorker.ProgressChanged, AddressOf PopulateDevicesWorkerProgressChanged
        AddHandler PopulateDevicesWorker.RunWorkerCompleted, AddressOf PopulateDevicesWorkerCompleted

        ' Start the BackgroundWorker
        If Not PopulateDevicesWorker.IsBusy Then
            PopulateDevicesWorker.RunWorkerAsync()
        End If

    End Sub

The worker will read a list of devices from a SQLite DB and (in the progresschanged event) populate an observable collection (AllDevices):

    Private Sub PopulateDevicesWorkerDoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs)

        ' We only continue if the \Clients\_Cache File exists and can be read
        If Not File.Exists(CacheFilePath) Then
            Exit Sub
        End If

        ' Create a new SQLite Connection & Connect to database
        Dim DBC As SQLiteDatabase = OpenDB(CacheFilePath)

        If Not IsNothing(DBC) Then

            ' Count the Rows in the \Clients\_Cache file
            Dim RowCount As Integer = CountTableRows(DBC, "_Cache")

            ' Set the SQL Query
            SqlQuery = "SELECT * FROM _Cache WHERE Archived = @Archived"

            ' Create the SQLite Command
            Using SQLitecmd As SQLiteCommand = New SQLiteCommand(SqlQuery, DBC.Connection)

                SQLitecmd.Parameters.AddWithValue(String.Empty & "Archived", IIf(fMain.ButtonItem_VIEWARCHIVE.Checked, "True", "False"))

                Using SQLiteReader = SQLitecmd.ExecuteReader()

                    Dim Counter As Integer = 0

                    ' Read All Properties into the Array
                    While SQLiteReader.Read()

                        Using DeviceItem As New DeviceItem

                            With DeviceItem

' Get the Device Info here

                            End With

                            ' Report progress at regular intervals
                            PopulateDevicesWorker.ReportProgress(CInt(100 * Counter / RowCount), DeviceItem)

                            ' Increment the Counter (for Progress)
                            Counter += 1

                        End Using

                    End While

                End Using

            End Using

        End If

        CloseDB(DBC)

    End Sub

Here is the WorkerProgressChanged event. It adds the current device (from the worker) into the observable collection (AlLDevices)

    Private Sub PopulateDevicesWorkerProgressChanged(sender As Object, e As ProgressChangedEventArgs)

        ' Update Status
        LabelItem_STATUS.Text = "Working.. (" & e.ProgressPercentage & "%)"

        ' Add the Device to Collection
        AllDevices.Add(TryCast(e.UserState, DeviceItem))

    End Sub

The WorkerCompleted event will set the objects in AllDevices to the OLV (DeviceListView1)

    Private Sub PopulateDevicesWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) ' Handles PopulateDevicesWorker.RunWorkerCompleted

' This is producing Cross-Thread error
        If Not IsNothing(_SelectedDevices) Then
            For Each item As ListViewItem In _SelectedDevices
                Debug.Print(item.Text)
            Next
        End If


        ' Populate the ObjectListView
        ControlHelper.ControlInvoke(DeviceListView1, Sub() DeviceListView1.SetObjects(AllDevices, True))

        ' Re-enable Form Updates
        ControlHelper.ControlInvoke(DeviceListView1, Sub() DeviceListView1.EndUpdate())

        ' If the refresh rate isn't already set, set it to the time taken to complete the Update PLUS the Seconds specified in the SETTINGS.INI File
        Dim difference As TimeSpan = DateTime.Now.Subtract(StartTime)
        If UpdateTimeInSeconds = -1 Then
            UpdateTimer.Interval = (RefreshRate + difference.TotalSeconds) * 1000
        End If

        ' Restart the Update Timer
        UpdateTimer.Start()

    End Sub

I was under the impression, that I can update the GUI (get the OLV selecteditems, etc.) from the WorkerProgressChanged and WorkerCompleted backgroundworker events but I get the darn cross-thread error.

I'm also having to INVOKE the BEGIN\END UPDATE as calling them directly produces error.

I have read that the olv.setobjects in ObjectListView 2.91 (the version I am using) should persist the selections but I haven't seen this at all.

Please! What am I missing? Its probably something daft or is there another way of doing this?


Solution

  • If you are not using a Forms.Timer (but Timers.Timer or Threading.Timer) for your UpdateTimer, anything called from the timer "tick" event will run on a different thread.

    Thus, PopulateDevices would also be called from a non GUI thread and the BackgroundWorker will run on that thread as well.