Adding nodes to treeview with Begin Invoke / Invoke - vb.net

I've been working through my first project and have had a great deal a valuable help from the guys on SO but now I'm stuck again.
The below sub is used to add TreeNodes to a TreeView, excluding certain filetypes/names, upon addition of new data:
Sub DirSearch(ByVal strDir As String, ByVal strPattern As String, ByVal tvParent As TreeNodeCollection)
Dim f As String
Dim e As String
Dim tvNode As TreeNode
Dim ext() As String = strPattern.Split("|"c)
Try
For Each d In Directory.GetDirectories(strDir)
If (UCase(IO.Path.GetFileName(d)) <> "BACKUP") And (UCase(IO.Path.GetFileName(d)) <> "BARS") Then
tvNode = tvParent.Add(IO.Path.GetFileName(d))
For Each e In ext
For Each f In Directory.GetFiles(d, e)
If (UCase(IO.Path.GetFileName(f)) <> "DATA.XLS") And (UCase(IO.Path.GetFileName(f)) <> "SPIRIT.XLSX") Then
tvNode.Nodes.Add(IO.Path.GetFileName(f))
End If
Next
Next
DirSearch(d, strPattern, tvNode.Nodes)
End If
Next
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
I'm now getting an error:
Action being performed on this control is being called from the wrong thread. Marshal to the correct thread using Control.Invoke or Control.BeginInvoke to perform this action.
On the following line:
tvNode = tvParent.Add(IO.Path.GetFileName(d))
Obviously, I understand its to do with 'threading' and the use of BeginInvoke / Invoke but even after reading the MSDN documentation on the error, I have no idea where to start.
This error only occurs, if I add a file to the initial directory (which is also the subject of a File System Watcher to monitor new additions).
Would someone be so kind as to give me an explanation in layman's terms so I may be able to understand.

This code is being run on a background thread where it's illegal to modify UI elements. The Invoke / BeginInvoke methods are ways to schedule a piece of code to run on UI thread where elements can be modified. For example you could change your code to the following
Dim action As Action = Sub() tvNode.Nodes.Add(IO.Path.GetFileName(f))
tvNode.TreeView.Invoke(action)
This code will take the delegate instance named action and run it on the UI thread where edits to tvNode are allowed
Fixing the earlier Add call is a bit trickier because there is no Control instance on which we can call BeginInvoke. The signature of the method will need to be updated to take a Dim control as Control as a parameter. You can pass in the TreeView for that parameter if you like. Once that is present the first Add can be changed as such
Dim outerAction As Action = Sub() tvNode = tvParent.Add(IO.Path.GetFileName(d))
control.Invoke(outerAction)

Related

vb.net Async I just don't get it

Public Function PiesTableTest(compairFile As String, version1 As String, Optional silent As Boolean = False) As Boolean
Dim dpgs As New frmDetailProgress
Dim retturn As Boolean
PiesThreadedTableTest(compairFile, version1, silent, dpgs)
End Function
Async Function PiesThreadedTableTest(compairFile As String, version1 As String, silent As Boolean, dpgs As frmDetailProgress) As Task(Of Boolean)
Dim ctl() As xmlControlAry
Dim xmlDoc As XElement
Dim xmlNodes As IEnumerable(Of XElement)
Dim notfound(0) As String
version = version1
nodeErrors = False
If Not silent Then
dpgs.lblTital.Text = "Pies Configuration Check"
dpgs.add("Pies Version = " & version)
dpgs.add("Loading Config Data....")
dpgs.Show()
End If
' load configuration data
GetPiesControl(ctl, version)
' load test xml file
xmlDoc = XElement.Load(compairFile)
xmlNodes = xmlDoc.Elements()
For Each ele As XElement In xmlNodes
NodeDrill("", ele, ctl, dpgs, notfound, silent)
Next
If nodeErrors And Not silent Then
dpgs.add("Testing done with Errors!!!", "R")
Else
dpgs.add("Testing Done NO ERRORS!", "G")
End If
Application.DoEvents()
If silent Then
dpgs.Dispose()
End If
'PiesThreadedTableTest = Not nodeErrors
If nodeErrors Then
Return False
Else
Return True
End If
End Function
I am trying to understand multi threading. frmDetailProgress is a "please wait " kind of form. and i have a animated gif on it. Plus it has a check box to close automatically after completion. Well the form is frozen till the process is done. I am trying to get the piesthreadedtabletest to run in another thread. I have read allot on this but i just don't understand the concept. I don't understand the await function enough to make this work. i get that await is designed to stop processing until something happens. But i want that form freed up to work. I get an error saying that the function will run synchronously unless i have an await - Why?
I got it working. It was a lack of understanding and i probably still need to learn more. I hope this will help someone in the future.
i created a class to call functions in the other class running in the second thread.
imports system.threading
public sub callThreadedProcedure()
dim tp as system.threading.thread ' this will be for the object running in the other thread
dim objectToRun as myclass ' this is the object you want to run in the thread
'this gets the object and puts it into the new thread
tp = new thread(sub() objectToRun.FunctionToRun(<put your parameters here if any>))
' start execution of the object in a new thread.
tp.start()
' that will get it to run in a separate thread. It works, there might be a better way
' and might not work in all situations, but for now it fixed my problem.
end sub
if you are trying to run functions in the original thread you need to pass a
reference to that object to the one in the second thread. you must then use invoke to run a function or sub from the second thread.
Invoke(sub() obj.function(<parameters>))
thanks Idle_mind invoked worked like it should.
I appreciate all that helped me along.

Updating Variable in Multithreading in VB.NET

I've wrote a program which on startup loads the computer list from Active Directory. This takes about 10 seconds. If the user has started the program with a specific host as parameter, it should be usable immediately.
So to don't interrupt the user I want to load the computer list in a different thread. The problem is that it writes to a variable (the computer list) which is also used in the main thread.
You may think, I could simply use a temporary variable and when its done overwrite the main variable. But I have to keep existing data of the main variable.
'hosts list
Private Shared hosts As New SortedDictionary(Of String, HostEntry)
'Get all computers in Active Directory
'Will run in a extra thread
Private Delegate Sub GetADcomputersDelegate()
Private Sub GetADcomputers()
If Me.InvokeRequired Then
Me.Invoke(New GetADcomputersDelegate(AddressOf GetADcomputers), Nothing)
Else
lblStatusAD.Text = "Getting Computers..."
Try
Dim search As New DirectorySearcher(ActiveDirectory.Domain.GetCurrentDomain().GetDirectoryEntry(), "(objectClass=computer)")
For Each host As SearchResult In search.FindAll()
'AddHost creates a new HostEntry object and adds it to my "global" hosts variable
'It also checks if a host is already present in the list and only updates it.
AddHost(host.GetDirectoryEntry().Properties("cn").Value.ToLower(), host.GetDirectoryEntry().Properties("description").Value)
Next
Catch ex As Exception
Debug.WriteLine("GetADcomputers() Exception: " & ex.Message)
End Try
ThreadPool.SetMaxThreads(hosts.Count, hosts.Count)
Dim ah As String = activehost
'Fill my ListBox with the computers
lstHosts.DataSource = New BindingSource(hosts, Nothing)
'Select the computer that was selected before
UseHost(ah)
lblStatusAD.Text = ""
End If
End Sub
So when GetADcomputers() runs in its own thread, the main thread is also blocked. I guess because auf the hosts variable.
So what could I change to make the thread do it's work and after that apply the updated computer list without losing data of entries in old hosts list? And all this in a fast and efficient way.
That code is very wrong. If you call that method on a secondary thread then it immediately marshals a call back to the UI thread and does EVERYTHING on the UI thread. What you should be doing is executing all the background work on the secondary thread and then marshalling to the UI thread ONLY to update the UI.
Get rid of that If...Else block and just make the entire body of the method what's current ly in the Else block. Next, identify all the lines that specifically interact with the UI and remove each of those to their own method. You then add If...Else blocks to each of those methods so that only the code that actually touches the UI is executed on the UI thread.
Here's a start:
Private Sub GetADcomputers()
UpdateStatusADLabel("Getting Computers...")
Try
Dim search As New DirectorySearcher(ActiveDirectory.Domain.GetCurrentDomain().GetDirectoryEntry(), "(objectClass=computer)")
For Each host As SearchResult In search.FindAll()
'AddHost creates a new HostEntry object and adds it to my "global" hosts variable
'It also checks if a host is already present in the list and only updates it.
AddHost(host.GetDirectoryEntry().Properties("cn").Value.ToLower(), host.GetDirectoryEntry().Properties("description").Value)
Next
Catch ex As Exception
Debug.WriteLine("GetADcomputers() Exception: " & ex.Message)
End Try
ThreadPool.SetMaxThreads(hosts.Count, hosts.Count)
Dim ah As String = activehost
'Fill my ListBox with the computers
lstHosts.DataSource = New BindingSource(hosts, Nothing)
'Select the computer that was selected before
UseHost(ah)
lblStatusAD.Text = ""
End Sub
Private Sub UpdateStatusADLabel(text As String)
If lblStatusAD.InvokeRequired Then
lblStatusAD.Invoke(New Action(Of String)(AddressOf UpdateStatusADLabel), text)
Else
lblStatusAD.Text = text
End If
End Sub

VB.Net Threading and Saving Results

I'm working on the program for work, where the user is able to search AD for a asset tag of a certain computer. If it is found it will give them a list in a listbox. I have all of that working, but when they do the search the UI freezes. I'm new to VB and OO for that matter. I understand that it is freezing because the search is running on the same thread as the UI, but I cannot for the life of me get another thread to do the work. When I tried to do the search in another thread I can't update the listbox because its not on the same thread. Any help would be greatly appreciated.
Function that searches AD:
Private Function searchAd()
'clear the results from previous entries
' AdResultListBox.Items.Clear()
Try
Dim rootEntry As New DirectoryEntry("GC://mydomaininfo")
Dim searcher As New DirectorySearcher(rootEntry)
'selects the Computer Name property
searcher.PropertiesToLoad.Add("cn")
Dim compname As String = PropertyTagTextbox.Text
'searches using wildcards
compname = "*" + compname + "*"
searcher.Filter = "(&(name=" + compname + ")(objectcategory=moreADinformation))"
Dim results As SearchResultCollection
results = searcher.FindAll()
Dim result As SearchResult
For Each result In results
'this is the part i'm having trouble with
Me.AdResultListBox.Items.Add(result.Properties("cn")(0)
Next
Catch ex As Exception
End Try
End Function
Private Sub ADSearchButton_Click(sender As Object, e As RoutedEventArgs) Handles ADSearchButton.Click
AdResultListBox.Items.Clear()
'create the new thread for searching
Dim SearchThread As New Thread(AddressOf searchAd)
SearchThread.Start()
End Sub
There happens to be an MSDN article "Using System.DirectoryServices to Search the Active Directory" which shows running it in another thread at http://msdn.microsoft.com/en-us/library/ms973834.aspx#dotnetadsearch_topic9.
This is how I typically do it. The Invoke function is part of the control and that passes a delegate to the UI thread so it can be processed in the correct thread.
Invoke(Sub
Me.AdResultListBox.Items.Add(result.Properties("cn")(0)
End Sub)
http://msdn.microsoft.com/en-us/library/zyzhdc6b.aspx

Accessing Form1 Properties From Thread

I have an exceptionhandler function that basically just writes a line to a textbox on Form1. This works fine when being run normally but the second I use a thread to start a process it cannot access the property. No exception is thrown but no text is written to the textbox:
Public Sub ExceptionHandler(ByVal Description As String, Optional ByVal Message As String = Nothing)
' Add Error To Textbox
If Message = Nothing Then
Form1.txtErrLog.Text += Description & vbCrLf
Log_Error(Description)
Else
Form1.txtErrLog.Text += Description & " - " & Message & vbCrLf
Log_Error(Description, Message)
End If
MessageBox.Show("caught")
End Sub
Is it possible to access a form's properties from a thread this way or would it be easier to write to a text file or similar and refresh the textbox properties every 10 seconds or so (Don't see this as a good option but if it's the only way it will have to do!).
Also, still new to VB so if I have done anything that isn't good practice please let me know!
No, you shouldn't access any GUI component properties from the "wrong" thread (i.e. any thread other than the one running that component's event pump). You can use Control.Invoke/BeginInvoke to execute a delegate on the right thread though.
There are lots of tutorials around this on the web - many will be written with examples in C#, but the underlying information is language-agnostic. See Joe Albahari's threading tutorial for example.
You have to use delegates. Search for delegates in VB.
Here a peace of code that does the job.
Delegate Sub SetTextCallback(ByVal text As String)
Public Sub display_message(ByVal tx As String)
'prüfen ob invoke nötig ist
If Me.RichTextBox1.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf display_message)
Me.Invoke(d, tx)
Else
tx.Trim()
Me.RichTextBox1.Text = tx
End If
End Sub

Why am I getting a "Cross-thread operation not valid exception" in the callback method when using AsyncCallback with BeginInvoke?

I have a windows form that gets data from a scale via the serial port. Since I need the operator to be able to cancel the process I do the get data process on a second thread.
The data collection process gets multiple readings from the scale one at a time. The form has a label that needs to be updated with information specific to each reading.
I call the method to get the data from the scale with this code.
Dim ad As New readALine(AddressOf thisScale.readALine)
Dim ac As New AsyncCallback(AddressOf Me.GetDataCallback)
Dim iar As IAsyncResult = ad.BeginInvoke("", ac, ad)
The delegate for the readALine method is defined in the UI code.
Delegate Function readALine(ByVal toSend As String) As String
The GetDataCallback method:
Private Sub GetDataCallback(ByVal ia As IAsyncResult)
.
.
.
lblInstructions.Text = _thisMeasure.dt.Rows(_currRow - 1).Item("cnt") & ":"
lblInstructions.Refresh()
.
.
.
End Sub
I get the exception on the "lblInstructions.Text =" statement.
I thought the GetDataCallback method was part of the UI thread so don't understand why I'm getting the exception. I know this could probably be rewritten using a BackgroundWorker and it's appropriate events but for now would like to understand why this isn't working as expected.
The application was written originally in VS2003 and just recently upgraded to VS2008.
Any insight will be appreciated.
Thanks,
Dave
The problem is a confusion over BeginInvoke. Calling Control.BeginInvoke marshals a delegate call to the UI thread. Calling BeginInvoke on a delegate causes it to be executed on a thread pool thread - and any callback you provide is executed on the same (thread pool) thread. The latter is what you're doing, which is why GetDataCallback is being executed on a thread pool thread, not the UI thread..
So, within GetDataCallback you need to call Control.Invoke or Control.BeginInvoke to marshal back to the UI thread.
One point to note: I very rarely use Control.InvokeRequired these days - it's simpler to unconditionally call Invoke/BeginInvoke; the performance difference isn't usually enough to make up for the benefit in readability, in my experience.
user-interface controls can only be updated by the thread that created them
try
yourForm.BeginInvoke
instead; that should marshall the call to the correct thread
In .NET 1.1 it was possible to perform these cross-thread operations, even though they weren't safe.
In .NET 2.0 the exception you mention is thrown, when you try to perform cross-thread operations such as this, which means that you are performing the UI operations on a non-UI thread, even though you thought you weren't.
Try using the Me.InvokeRequired method to determine whether you are currently on the UI thread. E.g. you could define a method to perform the necessary tasks:
Protected Sub PerformUIOperations()
If Me.InvokeRequired Then
'We are currently on a non-UI thread. Invoke this method on the UI thread:
Me.Invoke(New MethodInvoker(AddressOf Me.PerformUIOperations))
Return
End If
'Perform UI operations when we know it is safe:
lblInstructions.Text = "blabla"
End Sub
The PerformUIOperations method can then be called from any non-UI thread, since it takes care of performing the tasks on the correct thread itself.
Of course, if you need to pass parameters to the PerformUIOperations method (such as information regarding the ongoing operation) you'll have to define a delegate to match the PerformUIOperations method signature and use this instead of the MethodInvoker.
Jon,
I have the following in another section of the code:
Delegate Sub setValueCallback(ByVal row As Integer, ByVal value As Decimal)
Public Sub setValue(ByVal row As Integer, ByVal value As Decimal)
If Me.Controls.Item(_tbIndex(row - 1)).InvokeRequired Then
Dim d As New setValueCallback(AddressOf setValue)
Me.Invoke(d, New Object() {row, value})
Else
Dim tb As TextBox = Me.Controls.Item(_tbIndex(row - 1))
tb.Text = value
_dt.Rows(tb.Tag).Item(1) = value
End If
End Sub
How would this be rewritten to not use .InvokeRequired?
Dave, maybe this is the solution you are looking for:
Dim ad As New readALine(AddressOf thisScale.readALine)
Dim ac As New AsyncCallback(AddressOf Me.GetDataCallback)
Dim iar As IAsyncResult = ad.BeginInvoke("", ac, ad)
Delegate Function readALine(ByVal toSend As String) As String
Private Sub GetDataCallback(ByVal ia As IAsyncResult)
If lblInstructions.InvokeRequired Then
lblInstructions.Invoke(New AsyncCallback(AddressOf GetDataCallback), New Object() {ia})
Else
.
.
lblInstructions.Text = _thisMeasure.dt.Rows(_currRow - 1).Item("cnt") & ":"
lblInstructions.Refresh()
.
.
End If
End Sub