Events causing cross-thread error in backgroundworker_progresschanged and backgroundworker_complete - objectlistview

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.

Related

Catch event trigger from external application in Excel VBA

I use VBA to automate an external application that recently changed their COM API. The new API loads files asynchronously (used to be synchronous) so I need to wait for the file loaded trigger before I continue when I try to load a file.
I have tried the methods listed on the Microsoft website (EX1, EX2) which were also part of an accepted answer on StackOverflow.
Below is the code I have in a class module named UCExternal to contain the external application object:
Public WithEvents obj As External.Application
Private fileLoaded As Boolean
Private Sub obj_OnFileLoaded(ByVal lLayer As Long, ByVal strUNCPath As String)
Debug.Print lLayer
Debug.Print strUNCPath
fileLoaded = True
End Sub
Public Sub LoadSingleFile(fileStr As String)
fileLoaded = False
obj.LoadFile 0, fileStr
Do
DoEvents
Loop Until fileLoaded
End Sub
And then this is what I had in a normal code module to run using a button on the sheet:
Sub TryLoadFile()
Dim extObj as New UCExternal
set extObj.obj = CreateObject("External.Application")
filePath = "path/to/file"
extObj.LoadSingleFile filePath
End Sub
The event code never seems to fire and instead the Do Loop just runs until Excel crashes. I don't know if there is a way to confirm the application actually sent the event trigger? I have read through the new documentation for the application and that is the event they say to wait for. I have reached out to them for help as well but I wasn't sure if there was something more general I may have been missing. I have not worked with events external to Excel in the past. If I just step through it using the debugger and manually exit the Do Loop eventually the rest of the code that works on the loaded file works as well, so it does load the file.
extObj needs to be declared outside of TryLoadFile, or it will go out of scope and get cleared as soon as TryLoadFile completes
Dim extObj as New UCExternal
Sub TryLoadFile()
Set extObj = New UCExternal
set extObj.obj = CreateObject("External.Application")
filePath = "path/to/file"
extObj.LoadSingleFile filePath
End Sub

Check if Query Connection was successfully refreshed on VBA [duplicate]

In Excel 2016 VBA, I'm refreshing several queries like this:
MyWorkbook.Connections(MyConnectionName).Refresh
After the code is done, and no errors are encountered, I see that the hourglass icons for most of the queries are still spinning for several seconds.
Is it possible to check for success AFTER all the refreshes are completed? I'm concerned that my code isn't going to know if an error happens after the code finishes but before the queries are done refreshing.
BTW I don't want to do a RefreshAll, because some of the queries are dependent on others (uses them as a source). I refresh them in a certain sequence so that dependent queries are refreshed after the queries they are dependent on.
UPDATE:
I see that the Connection objects have a read-only RefreshDate property, which at first glance looked like it could be used to do this check:
MyWorkbook.Connections(MyConnectionName).OLEDBConnection.RefreshDate
HOWEVER, it doesn't seem to be getting set. I get an error trying to check it. If I set a Variant variable to that RefreshDate property, the variable shows as "Empty". The source is a SQL server database.
The QueryTable object exposes two events: BeforeRefresh and AfterRefresh.
You need to change your paradigm from procedural/imperative to event-driven.
Say you have this code in ThisWorkbook (won't work in a standard procedural code module, because WithEvents can only be in a class):
Option Explicit
Private WithEvents table As Excel.QueryTable
Private currentIndex As Long
Private tables As Variant
Private Sub table_AfterRefresh(ByVal Success As Boolean)
Debug.Print table.WorkbookConnection.Name & " refreshed. (success: " & Success & ")"
currentIndex = currentIndex + 1
If Success And currentIndex <= UBound(tables) Then
Set table = tables(currentIndex)
table.Refresh
End If
End Sub
Public Sub Test()
tables = Array(Sheet1.ListObjects(1).QueryTable, Sheet2.ListObjects(1).QueryTable)
currentIndex = 0
Set table = tables(currentIndex)
table.Refresh
End Sub
The tables variable contains an array of QueryTable objects, ordered in the order you wish to refresh them; the currentIndex variable points to the index in that array, for the QueryTable you want to act upon.
So when Test runs, we initialize the tables array with the QueryTable objects we want to refresh, in the order we want to refresh them.
The implicit, event-driven loop begins when table.Refresh is called and the QueryTable fires its AfterRefresh event: then we report success, and update the event-provider table object reference with the next QueryTable in the array (only if the refresh was successful), and call its Refresh method, which will fire AfterRefresh again, until the entire array has been traversed or one of them failed to update.
Just found this solution at Execute code after a data connection is refreshed
The bottom line is: Excel refreshes data connection in the background and thus the rest of the code is executed without interruption.
Solution: set BackgroundQuery property to False
Example:
For Each cnct In ThisWorkbook.Connections
cnct.ODBCConnection.BackgroundQuery = False
Next cnct
Possible problem: don't know which connection it is...
Remedy: case... when...
Dim cnct as WorkbookConnection ' if option explicit
' ODBC and OLE DB
For Each cnct In ThisWorkbook.Connections
Select case cnct.type
case xlconnectiontypeodbc
cnct.ODBCConnection.BackgroundQuery = False
case xlconnectiontypeoledb
cnct.OledbConnection.BackgroundQuery = False
end select
Next cnct
As you can see, code above only deals with ODBC and OLE DB. Depending on what types of data connection you are using, you can expand the select case clause. Unless changed, once run, connection's BackgroundQuery will remain off.

VBA best practices for modules relative to modeless userforms

I came across this similar issue and read the replies: Modeless form that still pauses code execution
I have been attempting to apply in my own situation the suggestion provided by David Zemens. In my situation, I cannot seem to find an approach that incorporates Mr. Zemen's suggestion without also utilizing a GoTo.
I am wondering if there is a better or more elegant solution.
Here is an outline of what I am doing:
I have a UserForm with a Command Button that begins the code execution that will perform several actions on multiple Excel workbooks. As such, there are a number of blocks of code and the successful completion of one block of code allows for the execution of the subsequent block of code.
At a certain point, depending on the situation, the code might require User input; in other situations, the needed data is obtainable from an Excel. If input is needed from the User, another UserForm is displayed.
The User may need to view several different Excel sheets before entering the input, so the UserForm is modeless. So the code comes to a stop until the User enters the needed input and clicks another Command Button.
It is at this point I am having trouble: how to resume the program flow. Is the only way to 'pick-up where it left-off' is by using a GoTo statement? Or is there some way to organize the modules so there is a single consistent program flow, defined in one spot and not duplicated from the point at which User input might be needed?
Here is my take on the problem . Hope I understood the problem correctly.
Assumptions:
There are two user forms.
UserForm1 with a button to start the processing.
UserForm2 with a button to supply intermediate input.
A sub inside a module to start/ launch UserForm1.
VBA Code (for the sub routine)
Sub LaunchUserForm1()
Dim frm As New UserForm1
'/ Launch the main userform.
frm.Show vbModeless
End Sub
VBA Code (for UserForm1)
Private Sub cmdStart_Click()
Dim i As Long
Dim linc As Long
Dim bCancel As Boolean
Dim frm As UserForm2
'/ Prints 1 to 5 plus the value returned from UserForm2.
For i = 1 To 5
If i = 2 Then
Set frm = New UserForm2
'/ Launch supplementary form.
frm.Show vbModeless
'<< This is just a PoC. If you have large number of inputs, better way will be
' to create another prop such as Waiting(Boolean Type) and then manipulate it as and when User
' supplies valid input. Then validate the same in While loop>>
'/ Wait till we get the value from UserForm2.
'/ Or the User Cancels the Form with out any input.
Do While linc < 1 And (linc < 1 And bCancel = False)
linc = frm.Prop1
bCancel = frm.Cancel
DoEvents
Loop
Set frm = Nothing
End If
Debug.Print i + linc
Next
MsgBox "User Form1's ops finished."
End Sub
VBA Code (for UserForm2)
Dim m_Cancel As Boolean
Dim m_prop1 As Long
Public Property Let Prop1(lVal As Long)
m_prop1 = lVal
End Property
Public Property Get Prop1() As Long
Prop1 = m_prop1
End Property
Public Property Let Cancel(bVal As Boolean)
m_Cancel = bVal
End Property
Public Property Get Cancel() As Boolean
Cancel = m_Cancel
End Property
Private Sub cmdlinc_Click()
'/Set the Property Value to 10
Me.Prop1 = 10
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'/ Diasble X button
Me.Cancel = True
Me.Hide
Cancel = True
End Sub
OK so here are my thoughts.
You have a userform frmSelectUpdateSheet which you wish to use in order to allow the user to select the sheet, when the sheet can't be determined programmatically. The problem is that if you do .Show vbModeless (which allows the user to navigate the worksheet/s), then code continues to execute which either leads to errors or otherwise undesired output.
I think it's possible to adapt the method I described in the previous answer. However, that's out of the question here unless you're paying me to reverse engineer all of your code :P
Assuming you have a Worksheet object variable (or a string representing the sheet name, etc.) which needs to be assigned at this point (and that this variable is Public in scope), just use the CommandButton on the form to assign this based on the selected item in the frmSelectUpdateSheet list box.
This is probably a superior approach for a number of reasons (not the least of which is trying to avoid application redesign for this sort of fringe case), such as:
This keeps your form vbModal, and does prevent the user from inadvertently tampering with the worksheet during the process, etc.
Using this approach, the thread remains with the vbModal displayed frmSelectUpdateSheet, and you rely on the form's event procedures for control of process flow/code execution.
It should be easier (and hence, cheaper) to implement; whether you're doing it yourself or outsourcing it.
It should be easier (and hence, cheaper) to maintain.
NOW, on closer inspection, it looks like you're already doing this sort of approach with the cmdbtnSelect_Click event handler, which leads me to believe there's a related/follow-up problem:
The sheet names (in listbox) are not sufficient for user to identify the correct worksheet. So if the user needs the ability to "scroll" the sheet (e.g., to review data which does not fit in the window, etc.), then add some spinner buttons or other form controls to allow them to navigate the sheet.

Emulate document locking in Lotusscript

In my Lotus Notes Application, when a user clicks an action, the action will call a run-on-server agent that will process the current document. The invoked agents sometimes doesn't run (which I think because of the concurrent agent limit of the server). This is why every 5 minutes there is a maintenance agent that runs to processed documents that are not processed by the invoked agents. The problem is, sometimes, a document is SIMULTANEOUSLY processed by these 2 agents, producing unacceptable results.
Is there a way I can emulate the document locking, such that documents can only be processed by one agent at a time? I don't like to use the native document locking because problems with the business rules might arise. I tried tagging the documents when one of the agents process it, then clears the flag after it is done. But the problem here is that there will still be a chance that the agents get hold of the document reference AT THE SAME TIME (due to the delay of saving the document, maybe).
Please help me. Thanks! :D
Yeah, that is not hard. Create a locking database where you have locking documents. They really only need to contain the UNID of the document being locked.
When your agents start processing a document, check if a locking document exists. If not, create one.
If there is one, either wait or skip the document for now.
After the document is done processing, delete the locking document.
This is trivial. Back when we were still on Notes/Domino 5, I even wrote a simple class to handle document locking in one of my application. The code below is referencing some functions and variables from another script library, but you get the idea. I am sure you can easily modify the code to work for you.
Option Public
Option Declare
Use "Functions.Globals"
Class DocumentLock
Private lockdb As NotesDatabase
Private lockview As NotesView
Private lockdoc As NotesDocument
Private lockservername As String
Private lockdbname As String
Private lnpdoc As NotesDocument ' Document to lock/unlock
Public Sub New(doc As NotesDocument)
me.lockservername = globals.GetValue("LockServer")
me.lockdbname = AppHomeDir + globals.GetValue("LockDBname")
If me.lockdb Is Nothing Then
Set me.lockdb = New NotesDatabase(me.lockservername, me.lockdbname)
End If
Set me.lockview = me.lockdb.GetView("LockedDocs")
Call me.lockview.Refresh()
Set me.lnpdoc = doc
End Sub
Public Sub LockMe()
Set me.lockdoc = New NotesDocument(me.lockdb)
me.lockdoc.Form="Locked"
me.lockdoc.LockUNID=me.lnpdoc.UniversalID
me.lockdoc.LockUser= globalcurrentusername
me.lockdoc.LockTime=Str(Now())
me.lockdoc.ClaimNumber = me.lnpdoc.GetItemValue("ClaimNumber")(0)
me.lockdoc.DocumentForm = me.lnpdoc.GetItemValue("Form")(0)
Call me.lockdoc.Save(True,True)
End Sub
Public Sub UnlockMe()
Call me.lockview.Refresh()
Set me.lockdoc = me.lockview.GetDocumentByKey(me.lnpdoc.UniversalID)
If Not me.lockdoc Is Nothing Then
Call me.lockdoc.Remove(True)
Call me.lockview.Refresh()
End If
End Sub
Public Function IsLocked(flagShowInfo As Boolean) As Boolean
Call lockview.Refresh()
Set me.lockdoc = me.lockview.GetDocumentByKey(me.lnpdoc.UniversalID)
If me.lockdoc Is Nothing Then
me.IsLocked = False
Else
me.IsLocked = True
If flagShowInfo = True Then
MsgBox "Document locked " & locktext & "." & Chr$(13) & "Please wait a while and try again.."
End If
End If
End Function
Public Function LockText() As String
LockText = "by " & LockUserName() & " at " & me.lockdoc.LockTime(0)
End Function
Public Function LockUserName() As String
Dim lockedby As String
lockedby = me.lockdoc.LockUser(0)
If lockedby = globalcurrentusername Then
LockUserName = "you"
Else
LockUserName = lockedby
End If
End Function
End Class

System.timers.timer - unable to update label with time elapsed

I want to display time elapsed on Label control on windows form.
for that i am using System.Timers.timer but unable to update Label with the elapsed time on button click event.
For example
Private Shared mtimer As System.Timers.Timer
Private Sub btnprocess_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnprocess.Click
'Counter to Calculate time as Process Starts
mDate = Date.Now
''Create a timer with second interval
mtimer = New System.Timers.Timer()
''Hook the Elapsed event
**AddHandler mtimer.Elapsed, AddressOf Processtick**
mtimer.Start()
'set the interval 1 second
mtimer.Interval = 1000
mtimer.Enabled = True
''Some functions
end sub
Private Sub Processtick(ByVal source As Object, ByVal e As ElapsedEventArgs)
Dim ts As TimeSpan = DateTime.Now.Subtract(mDate)
lblelapsed.Text = ts.Hours & ":" & ts.Minutes & ":" & ts.Seconds
End sub
tried above code but it doesnt work,
i want to update elapsed time on label control as soon as user click Process button
till all the functions get executed on button click.
Please help.
Since System.Timers.Timer runs on a separate thread it cannot directly update the GUI. You'll need to use BeginInvoke on the control to forward updates to the GUI thread. Unfortunately I'm not really familiar with vb.net, but you can find an example of how to do it here: http://msdn.microsoft.com/en-us/library/0b1bf3y3.aspx

Resources