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?
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.