Question Thread & Listview

freddyboy

Active member
Joined
Jun 18, 2020
Messages
27
Programming Experience
Beginner
Good day, I am trying invoke a Listview and I can't get it to work.
I am getting the cross-thread error at line 31
Inspfolder is my variable for passing filepath from an excel worksheet.
I manage to get it working if I use:
Process.Start("explorer.exe", String.Format(Inspfolder))
But I'd rather have the whole folder content into my listview if someone can help me figuring how to pass it correcly?


VB.NET:
Imports System.Threading

Imports System.IO

Dim MonThread As New Thread(AddressOf Rangeloops) 'Thread for loops
    Public Delegate Sub Callback(ByVal s As Control, ByVal v As Object)
  
Private Sub UpdateUI(ByVal this_Control As Control, ByVal vRange As Object)

this_Control.Text = CType(vRange, String)

End Sub

Private Sub ExecuteWork(ByVal value As String)

        TextBox_selectednodes.Invoke(New Callback(AddressOf UpdateUI), TextBox_selectednodes, value)    'TreeView_Insp_Records.SelectedNode.Text = TextBox_selectednodes

        Dim s As String = "Testing Worker Method"
        Dim i As Long
        Dim values As Object(,) = rg1.Value 'Range for fred

        For i = 1 To values.GetUpperBound(0) 'rows

            Dim Inspfolder As String

            If values(i, 3) = value Then

                TextBox1.Invoke(New Callback(AddressOf UpdateUI), TextBox1, values(i, 33))
                Inspfolder = TextBox1.Text

                ListBox1.Invoke(New Callback(AddressOf UpdateUI), ListBox1.Items.Add(Directory.GetFiles(Inspfolder)))
    
       Exit Sub
            End If
        Next
                 Private Sub TreeView_Insp_Records_AfterSelect(sender As System.Object, e As System.Windows.Forms.TreeViewEventArgs) Handles TreeView_Insp_Records.AfterSelect

        Dim thread As New Thread(Sub() ExecuteWork(e.Node.Text))
        thread.Start()
          End Sub

 Private Sub Rangeloops()
   Dim i As Long

        Dim values As Object(,) = rg1.Value
        For i = 1 To values.GetUpperBound(0) 'rows 
            Dim Inspfolder As String

            If values(i, 3) = TreeView_Insp_Records.SelectedNode.Text Then 

                TextBox1.Text = values(i, 33)
                Inspfolder = TextBox1.Text

                ListBox1.Items.AddRange(Directory.GetFiles(Inspfolder))
   Exit Sub
            End If
        Next
 
Last edited:
EDIT:

I just realised that it's a ListBox rather than a ListView but I think that everything I said still stands.

ORIGINAL:

The whole point of the Invoke call is so that you don't access members of the control on any but the secondary thread. Think about what that line of code is actually doing. It's adding an item to the ListView BEFORE it calls Invoke. That's no use.

Think about what you're actually trying to achieve. How can that UpdateUI method be of any use there? It expects to receive a control and to set the Text of that control. How does adding items to a ListView relate to that?

Further more, that Add method will add a single item to the ListView. How does it make sense to get an array of file paths and then add that as a single item? Again, think about what you're actually trying to achieve. Break it into steps and then implement each step. I have no idea what algorithm that code would be implementing and I'll wager that you don't either.
 
Last edited:
Firstly, if the intention is to get a list of file paths and add them each as an item then you need to be calling AddRange rather than Add. Secondly, adding the items is accessing the UI so that needs to be done on the UI thread, That might look like this:
VB.NET:
Private Sub AddListItems(list As ListBox, items As String())
    list.Items.AddRange(items)
End Sub
and:
VB.NET:
ListBox1.Invoke(New Action(Of ListBox, String())(AddressOf AddListItems),
                ListBox1,
                Directory.GetFiles(folderPath))
Of course, you can just use a Lambda and not need the extra methods:
VB.NET:
ListBox1.Invoke(Sub(items As String()) ListBox1.Items.AddRange(items),
                Directory.GetFiles(folderPath))
 
Ok, so I went to read about UI thread and this part much more clear in my head. I did as you mentionned and added:

Private Sub AddListItems(list As ListBox, items As String())
list.Items.AddRange(items)
End Sub

Now the second part is still vague for me, I tried calling the Invoke inside my 2nd thread with:

ListBox1.Invoke(Sub(items As String()) ListBox1.Items.AddRange(items), Directory.GetFiles(folderPath))

I change folderPath for Inspfolder but I got an error. I am going swimming a bit to clear my head a bit and bbl tonight.
 
Getting an error of "Incorrect parameter numbers" (TargetParameterCountException) at line 48
I'm confuse... not sure I can figure this out. Let me know if sending the whole code would help...

To explain what I'm trying to do:

I have an excel worksheet holding a bunch of inspections. Each inspections have a column in which I entered the file path where to find all the related files for those inspections.
Inspfolder is the variable holding that filepath (Exemple: D:\Fred\Souvenirs)

I am trying to show those files in the listbox.

Once I get through this step, I should be close to finish this project.

I am adding a printscreen in case it helps.

1.png



VB.NET:
Imports System.Threading
Imports System.IO

Public Class RSIform

    Dim MonThread As New Thread(AddressOf Rangeloops) 'Thread for loops
    Public Delegate Sub Callback(ByVal s As Control, ByVal v As Object)

    Dim Thread2 As New Thread(AddressOf Clearalltxtboxthread) 'Thread for clearing all textbox
   
      Public Sub Clearalltextboxthread()

        Dim thread2 As New Thread(Sub() Clearalltxtboxthread(String.Empty))
        thread2.Start()

    End Sub
   
     Private Sub AddListItems(list As ListBox, items As String())
     
        list.Items.AddRange(items)
       
    End Sub
   
      Private Sub UpdateUI(ByVal this_Control As Control, ByVal vRange As Object)

        this_Control.Text = CType(vRange, String)
       
    End Sub
   
     Private Sub ExecuteWork(ByVal value As String)

        TextBox_selectednodes.Invoke(New Callback(AddressOf UpdateUI), TextBox_selectednodes, value)
       
        Dim i As Long

        Dim values As Object(,) = rg1.Value 'Range for fred

        For i = 1 To values.GetUpperBound(0) 'rows

            Dim Inspfolder As String

            If values(i, 3) = value Then

                TextBox1.Invoke(New Callback(AddressOf UpdateUI), TextBox1, values(i, 33))

                Inspfolder = TextBox1.Text

                ListBox1.Invoke(Sub(items As String()) ListBox1.Items.AddRange(items), Directory.GetFiles(Inspfolder))
                Exit Sub
            End If
        Next
    End Sub  
   
     Private Sub TreeView_Insp_Records_AfterSelect(sender As System.Object, e As System.Windows.Forms.TreeViewEventArgs) Handles TreeView_Insp_Records.AfterSelect

        Dim thread As New Thread(Sub() ExecuteWork(e.Node.Text))
        thread.Start()
           End Sub
         
            Private Sub Rangeloops()
              Dim row As Long
        row = 1
        Dim Inspfred As Worksheet
        Inspfred = Form1.xlWorkBook.Sheets("Inspfred")
        Dim rg1 As Range 'Range pour fred
        rg1 = Inspfred.Range("C1").CurrentRegion
         Dim i As Long

        Dim values As Object(,) = rg1.Value


        For i = 1 To values.GetUpperBound(0) 'rows

            Dim Inspfolder As String


            If values(i, 3) = TreeView_Insp_Records.SelectedNode.Text Then

                TextBox1.Text = values(i, 33)
                Inspfolder = TextBox1.Text
                  ListBox1.Items.AddRange(Directory.GetFiles(Inspfolder))
                    Exit Sub
            End If
        Next
 
Last edited:
You really need to do a bit of reading on multi-threading and delegates. The basic principle is that there are only a few members of any control that are safe to access on a thread other than the one that owns their window handle, which will generally be the app's main thread. Those members include the following:
  • InvokeRequired property: True if the current thread does not own the control's handle and False if it does. Effectively tells you whether it is unsafe to access members of the control on the current thread.
  • Invoke method: Invokes a delegate on the thread that owns the control's handle and waits for the referenced method to complete before returning, i.e. invokes a delegate synchronously.
  • BeginInvoke method: Invokes a delegate on the thread that owns the control's handle and returns immediately, i.e. invokes a delegate asynchronously.
A delegate is an object that refers to a method. You can pass a delegate around like you can any other object and then invoke it anywhere. Invoking it means executing the method it refers to, but that can be done in places that have no knowledge of the object the method is a member of.

You can create a delegate in two different ways. The AddressOf operator will create a delegate to a named method. Alternatively, you can use Sub or Function to create a Lambda expression, which is an anonymous method. Here's a verbose example of updating the Text of a control using a named method:
VB.NET:
Private Sub SetControlText(cntrl As Control, text As String)
    If cntrl.InvokeRequired Then
        'We are currently executing on a secondary thread'
        'Invoke the current method a second time on the UI thread'
        cntrl.Invoke(New Action(Of Control, String)(AddressOf SetControlText), cntrl, text)
    Else
        'We are currently executing on the UI thread'
        'Access the control directly'
        cntrl.Text = text
    End If
End Sub
You can then call that method anywhere on any thread and it will just work:
VB.NET:
SetControlText(TextBox1, "Hello World")
If you call it on the UI thread then it jumps immediately to the Else block and the control is updated. If you call it on a secondary thread then the If block is executed and the method is invoked for a second time on the UI thread. The control gets updated in that second call and then that call completes, Invoke returns and the first call completes too.

Alternatively, you can simplify that method and do the invocation where you call it:
VB.NET:
Private Sub SetControlText(cntrl As Control, text As String)
    cntrl.Text = text
End Sub
and:
VB.NET:
TextBox1.Invoke(New Action(Of Control, String)(AddressOf SetControlText), TextBox1, "Hello World")
If you don't need or want such a method to be reusable then you can do away with the named method and AddressOf and use a Lambda instead:
VB.NET:
TextBox1.Invoke(Sub(cntrl As Control, text As String) cntrl.Text = text, TextBox1, "Hello World")
That can actually be simplified because a Lambda has access the the same variables, etc, as the context it's declared in. That means that there's no need pass variables as arguments because they can be used directly in the Lambda:
VB.NET:
TextBox1.Invoke(Sub() TextBox1.Text = "Hello World")
In your case, you should spend some time to understand all this first, to avoid flailing around and trying random stuff and possibly breaking your code even more. Once you understand what's going on, you should decide what is the most logical way to create your delegates. If you find that you're doing the same thing in multiple places then a named method and AddressOf is the way to go. You may choose to go that way in every case for consistency. If you are doing something in one place only then a Lambda is appropriate, but if you use any Lambdas then you should be consistent and use them everywhere unless a named method is specifically beneficial.
 
It may help to think about threading in GUI apps like you have two rooms:
  • One is a physical room where you have controls that can display information and receive information from user.
  • The other is an abstract thinking room where you can process information and request information from other sources like files, databases, web and so on.
Only information can pass between these rooms, not controls, because controls are not allowed in the thinking room.
  • When you want to process information you first gather information from the controls and pass it to the thinking room.
  • Invoke is simply a way to start a method that can operate in the physical room, and information can be passed along when this method is called.
Here's a basic example how this may play out:
VB.NET:
Private Sub PhysicalRoomStart()
    Dim information = TextBoxInput.Text
    Dim think As New Threading.Thread(AddressOf ThinkingRoom)
    think.Start(information)
End Sub

Private Sub ThinkingRoom(information As String)
    Dim result = $"This information has length {information.Length}"
    Invoke(Sub() PhysicalRoomResult(result))
End Sub

Private Sub PhysicalRoomResult(information As String)
    LabelResult.Text = information
End Sub
You can expand the "information" to any and all information that needs to be passed to/from these rooms.
 
I have so many questions when I read all this...
I guess I'll give it another try, but this is so over my skills.
Here is what I've done as a 1st step:

VB.NET:
'This is your exemple:

Private Sub SetControlText(cntrl As Control, text As String)
    cntrl.Text = text
End Sub

'I addaped to mine as follow:

Private Sub AddListItems(ByVal list As ListBox, ByVal items As String())
        list.Items.AddRange(items)
End Sub

'And I created a Delegate:

Public Delegate Sub Callback2(ByVal list As ListBox, ByVal items As String)

Does that look right ?
 
Those methods look fine but there's no point declaring your own delegates these days. Use the MethodInvoker delegate for Subs with no parameters, a generic Action delegate for Subs with 1 to 16 parameters or a generic Func delegate for Functions with zero to 16 parameters.
 
It's the only way I understand them for now.
Now, if I stick to what I just did.
My invoke looks like this:
VB.NET:
ListBox1.Invoke(New Callback2(AddressOf AddListItems), ListBox1.Items.AddRange(Directory.GetFiles(Inspfolder)))
But AddlistItems is underline with the following error:
Error 1 Method 'Private Sub AddListItems(list As Microsoft.Office.Interop.Excel.ListBox, items() As String)' does not have a signature compatible with delegate 'Delegate Sub Callback2(list As Microsoft.Office.Interop.Excel.ListBox, items As String)'.

or I can try as you suggested and delete the Delegate and use this Invoke instead:
VB.NET:
 ListBox1.Invoke(Sub(items As String()) ListBox1.Items.AddRange(items), Directory.GetFiles(Inspfolder))
But I get the following error:
2.PNG
 
Last edited:
I changed
VB.NET:
Private Sub AddListItems(ByVal list As ListBox, ByVal items As String())
for
VB.NET:
 Private Sub AddListItems(ByVal list As ListBox, ByVal items As String)

And now it say's that ListBox1.Items.AddRange(Directory.Getfiles(Inspfolder))) does not produce a value
VB.NET:
  ListBox1.Invoke(New Callback2(AddressOf AddListItems), ListBox1.Items.AddRange(Directory.GetFiles(Inspfolder)))
 
I changed
VB.NET:
Private Sub AddListItems(ByVal list As ListBox, ByVal items As String())
for
VB.NET:
 Private Sub AddListItems(ByVal list As ListBox, ByVal items As String)
And why would you do that? That is the exact opposite of what you should have done. This is another example of blindly doing something without thinking about what you actually want to achieve. The idea here is that you have multiple items that you want to add to a ListBox. Why would a String be a better option for that that a String array? A String array contains multiple Strings so that is the obvious choice. There's also the fact that Directory.GetFiles returns a String array, so that's what you're starting with. I understand that you're new to this but being new to programming is not new to logic. Think about what you've got and what you're trying to achieve before writing code to go from one to the other.
 
And now it say's that ListBox1.Items.AddRange(Directory.Getfiles(Inspfolder))) does not produce a value
VB.NET:
  ListBox1.Invoke(New Callback2(AddressOf AddListItems), ListBox1.Items.AddRange(Directory.GetFiles(Inspfolder)))
You've just completely ignored the contents of post #3 there. Firstly, the whole point of calling Invoke is so that you access members of the control on the UI thread. What is the point of that if you then go and access members of the control before calling Invoke? You might think about expanding your code out into multiple lines so you have a clear idea of what individual steps are being performed. That code is equivalent to this:
VB.NET:
Dim var1 = Directory.GetFiles(Inspfolder)
Dim var2 = ListBox1.Items.AddRange(var1)
Dim var3 = New Callback2(AddressOf AddListItems)

ListBox1.Invoke(var3, var2)
That makes it blatantly obvious that you're accessing members of the ListBox on the secondary thread before calling Invoke, which is exactly what calling Invoke is supposed to avoid!

Apart from that, the method you're invoking is intended to add the items to the ListBox, so why are you adding the items to the ListBox before invoking it? As the error message says, AddRange doesn't return anything and why would it? If you had taken notice of post #3 then you'd know that you are supposed to get the file paths on the secondary thread, then pass that to the delegate that gets invoked on the UI thread:
VB.NET:
Dim var1 = Directory.GetFiles(Inspfolder)
Dim var2 = New Callback2(AddressOf AddListItems)

ListBox1.Invoke(var2, var1)
The method you invoke and the delegate you use to invoke it both need to have a parameter of type String array because that is what you're passing in that contains the items to add.
 
Back
Top