Beginner trying cross threading...

nineclicks

Member
Joined
Jun 13, 2013
Messages
18
Programming Experience
Beginner
VB.NET:
    Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
        ListView1.Items.Clear()

        For Each Thing As String In Directory.GetFiles(e.Node.Tag)
            Dim fi As FileInfo = New FileInfo(Thing)
            With ListView1.Items.Add(fi.Name)
                .ImageIndex = 0
                .Tag = fi.FullName
            End With
        Next

        For Each LVI As ListViewItem In ListView1.Items
            Dim shellFile__1 As ShellFile = ShellFile.FromFilePath(LVI.Tag)
            Dim shellThumb As Bitmap = shellFile__1.Thumbnail.ExtraLargeBitmap
            ImageList2.Images.Add(shellThumb)

            LVI.ImageIndex = ImageList2.Images.Count - 1
        Next

    End Sub

I need to put that second For loop into its own thread, it's loading thumbnails. I've gone through some tutorials to do multithreading and crossthreading but I don't understand how I would enqueue things like "For Each LVI As ListViewItem In ListView1.Items" let alone the whole loop. How would I put that part in its own thread?

Thanks very much.
 
Hi,

If you are just starting out with Multi-Threading then I would suggest that you look into the BackGroundWorker Class for starters. Have a look here:-

BackgroundWorker Class (System.ComponentModel)

If you then want to pass the contents of a ListView to a BackGroundWorker then you need to create a Collection of the ListViewItems and then pass that collection as an Argument to the BackGroundWorker through its RunWorkerAysnc Method. i.e:-

VB.NET:
Public Class Form1
 
  Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
    'Just make up a ListView of Test Items
    For Counter As Integer = 1 To 10
      Dim LVI As New ListViewItem
      LVI.Text = "Test"
      LVI.SubItems.Add("Test")
      ListView1.Items.Add(LVI)
    Next
  End Sub
 
  Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    'Create a new Collection of ListViewItems and Pass as an Argument to the BackGroundWorker
    Dim myLVICollection As List(Of ListViewItem) = ListView1.Items.Cast(Of ListViewItem).ToList
    BackgroundWorker1.RunWorkerAsync(myLVICollection)
  End Sub
 
  Private Sub BackgroundWorker1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    Dim myListViewItems As List(Of ListViewItem) = DirectCast(e.Argument, List(Of ListViewItem))
 
    For Each LVI As ListViewItem In myListViewItems
      MsgBox(LVI.Text)
    Next
  End Sub
End Class

Hope that helps.

Cheers,

Ian
 
You can't put that loop in its own thread. Anything that involves the UI must be done on the UI thread. That said, it's only the part of it that directly involves the UI that needs to be done on the UI thread. The rest can be done on a secondary thread. So, what you need to do is break it up into three parts:

1. Get the data you require from the UI. In this case, that would mean looping through the items in the ListView and getting the Tag of each one and putting it somewhere that you can use later. The obvious choice would be a String array.

2. Do all the work that doesn't directly involve the UI using the data from step 1. In this case, that would be loading the Images.

3. Update the UI with the data from step 2. In this case, that would be associating each ListViewItem with the appropriate Image.

Get that working in a single thread first, i.e. replace your existing loop with three separate sections of code that implement those three steps. Once you've got that working, you can simply break the second section out into a separate thread.

With regards to step 2, you may or may not be able to actually add the Images to the ImageList on the secondary thread. If the ImageList is not associated with the ListView then you definitely can. If the ImageList is associated with the ListView but you don't actually try to associated an Image with a ListViewItem on the secondary thread then I'm not too sure either way, so you'd have to test that.

What I would suggest is that you actually put those three sections of code into three separate methods. You can then just call each of those three methods where your current loop is. Once it's working, the multi-threading part is very easy:

1. Add a BackgroundWorker and handle its DoWork and RunWorkerCompleted events.
2. After your call to the first of your three methods, call the BGW's RunWorkerAsync method and pass the data you retrieved in that first method.
3. Move the call to your second method into the DoWork event handler. Get the input from the e.Argument property and assign the output to the e.Result property.
4. Move the call to your third method into the RunWorkerCompleted event handler. Get the output from the e.Result property.

Here's some useful information on how to use a BackgroundWorker and pass data between threads:

Using the BackgroundWorker Component

When it comes time to actually update your ListView, make sure you call its BeginUpdate method first and its EndUpdate method when you're done.

Now, there's quite a bit of information there but don't let it overwhelm you. Each step is fairly simple so take it one step at a time and don't move on to the next until the previous is done and working. Certainly implementing the first three steps is something you can do because you've already done it. You just need to expand out your existing code. Start with the step 1. When it's done, move on to step 2, etc. I'm more than willing to help with issues along the way but I won't be providing any more information on any stage there if you haven't already completed the stages before it. That would be counter-productive.
 
I'm closer... I think. But I'm still having trouble getting the bitmaps into the ImageList. Imagelist does not have an invoke method so I'm not sure what to do.

I know my programming is extremely messy, and I added a bit to get the icons to come out without the imagelist smashing them into a square. Please bare with my code.

I tested it without any threading so I know there is no problem with the bit I added. Basically now I'm working with "CropImage" instead of "shellThumb". The very last line is giving me the crossthreading error.

Thanks for you help so far and your patience... Am I on the right track? I know I can probably remove some redundancy with the tags but am I any closer to getting this working?

VB.NET:
Dim tagArray As New ArrayList

    Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
        ListView1.Items.Clear()
        ImageList2.Images.Clear()
        For Each Thing As String In Directory.GetFiles(e.Node.Tag)
            Dim fi As FileInfo = New FileInfo(Thing)
            With ListView1.Items.Add(fi.Name)
                .ImageIndex = .Index
                .Tag = fi.FullName
            End With
        Next
        getTags()
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub getTags()
        For Each LVI As ListViewItem In ListView1.Items
            tagArray.Add(LVI.Tag)
        Next
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        For Each thing In tagArray
            Dim shellFile__1 As ShellFile = ShellFile.FromFilePath(thing)
            Dim shellThumb As Bitmap = shellFile__1.Thumbnail.MediumBitmap

            Dim CR0 As Integer
            Dim CR1 As Integer
            Dim CR2 As Integer
            Dim CR3 As Integer
            If shellThumb.Width = shellThumb.Height Then
                CR0 = 0
                CR1 = 0
                CR2 = shellThumb.Width
                CR3 = shellThumb.Height
            ElseIf shellThumb.Width > shellThumb.Height Then
                CR0 = 0
                CR1 = -(shellThumb.Width - shellThumb.Height) / 2
                CR2 = shellThumb.Width
                CR3 = shellThumb.Width
            ElseIf shellThumb.Width < shellThumb.Height Then
                CR0 = (shellThumb.Width - shellThumb.Height) / 2
                CR1 = 0
                CR2 = shellThumb.Height
                CR3 = shellThumb.Height
            End If
            Dim CropRect As New Rectangle(CR0, CR1, CR2, CR3)

            Dim CropImage = New Bitmap(CropRect.Width, CropRect.Height)
            Using grp = Graphics.FromImage(CropImage)
                grp.DrawImage(shellThumb, New Rectangle(0, 0, CropRect.Width, CropRect.Height), CropRect, GraphicsUnit.Pixel)
            End Using

            ImageList2.Images.Add(CropImage)

        Next
    End Sub
 
First of all, don't use an ArrayList. They have no place in .NET 2.0 or later. If you want a simple collection then use a List(Of T), where T is the type of the items you want to store. You're storing Strings so it would be a List(Of String). In this case though, you should be using an array rather than a collection. You know exactly how many items there will be up front because you know how many ListViewItems there are and the number won't be changing. As such, you don't need the dynamic resizing that a collection offers. Just use a For loop instead of a For Each loop.

Also, as I already said and as I showed in that link I provided, you should be passing the array of file names as an argument when you call RunWorkerAsync and getting it back in the DoWork event handler from the e.Argument property.

As for adding the Images to the ImageList, I said earlier that I wasn't sure whether it would be possible to do it on the secondary thread or not and this shows that it's not. As such, you should pass the Images out in basically the same way as you pass the file names in: put the Images in an array and assign that to the e.Result property in the DoWork event handler and get it back from the e.Result property in the RunWorkerCompleted event handler. You can then use AddRange to add the whole array to the ImageList in one go.

Note that the DoWork event handler is executed on a secondary thread, so you can't do anything there that requires access to the handle of a control. Because the ImageList is associated with the ListView, any change to the ImageList affects the ListView, which is not allowed. The RunWorkerCompleted event handler is executed on the UI thread, so you can update the UI there, which is the whole point.
 
I should mention I haven't done much programming in years and I was never particularly proficient...

Anyway, the Listview will regularly be loading new file paths into it. If I have to define the array outside of a sub, how to I define it's size and what do I do when the size changes? Since I am loading different lists, do I need a List(of T)?

I decided to give List(of T) a try and it is working, but please let me know if I could be doing it better.

VB.NET:
Dim tagArray As New List(Of String)
    Dim bitmapArray As New List(Of Bitmap)

    Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
        ListView1.Items.Clear()
        ImageList2.Images.Clear()
        For Each Thing As String In Directory.GetFiles(e.Node.Tag)
            Dim fi As FileInfo = New FileInfo(Thing)
            With ListView1.Items.Add(fi.Name)
                .ImageIndex = .Index
                .Tag = fi.FullName
            End With
        Next
        getTags()
        BackgroundWorker1.RunWorkerAsync()

    End Sub

    Private Sub getTags()
        For Each LVI As ListViewItem In ListView1.Items
            tagArray.Add(LVI.Tag)
        Next
    End Sub

 Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        For Each thing In tagArray
            Dim shellFile__1 As ShellFile = ShellFile.FromFilePath(thing)
            Dim shellThumb As Bitmap = shellFile__1.Thumbnail.MediumBitmap

            Dim CR0 As Integer
            Dim CR1 As Integer
            Dim CR2 As Integer
            Dim CR3 As Integer
            If shellThumb.Width = shellThumb.Height Then
                CR0 = 0
                CR1 = 0
                CR2 = shellThumb.Width
                CR3 = shellThumb.Height
            ElseIf shellThumb.Width > shellThumb.Height Then
                CR0 = 0
                CR1 = -(shellThumb.Width - shellThumb.Height) / 2
                CR2 = shellThumb.Width
                CR3 = shellThumb.Width
            ElseIf shellThumb.Width < shellThumb.Height Then
                CR0 = (shellThumb.Width - shellThumb.Height) / 2
                CR1 = 0
                CR2 = shellThumb.Height
                CR3 = shellThumb.Height
            End If
            Dim CropRect As New Rectangle(CR0, CR1, CR2, CR3)

            Dim CropImage = New Bitmap(CropRect.Width, CropRect.Height)
            Using grp = Graphics.FromImage(CropImage)
                grp.DrawImage(shellThumb, New Rectangle(0, 0, CropRect.Width, CropRect.Height), CropRect, GraphicsUnit.Pixel)
            End Using

            bitmapArray.Add(CropImage)

        Next
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
        For Each bmp As Bitmap In bitmapArray
            ImageList2.Images.Add(bmp)
        Next
        bitmapArray.Clear()
        tagArray.Clear()
        ListView1.Refresh()
    End Sub

Another little thing is the line ".ImageIndex = .Index + 2". I had to use +2 to get them to line up right but I'm not sure why. I would have figured they both started at zero.
Nevermind, figured out the problem and updated the code...
 
Last edited:
I also should mention that at some point I would like to be able to do two extra things here... One, update after maybe say every 5 seconds or so if there's so many files that it takes a really long time it can at least start displaying the ones that are ready. And two start loading somewhere in the middle if the user scrolls down or selects a file.

Is there a way to make my own object list that has a bitmap, tag and index property to make it easier to juggle these things? As it is right now I am simply replying on the fact that the bitmap list will have loaded the files in the same order as they appear. It would be nice if I had a list of objects that I could apply an index number to (that corresponds to the listview index) copy the tag over and load the bitmaps into. That way I can do it in a different order and not lose my place. How would I best go about this?

Thanks so much for your help so far, I'm already way further than I expected to be this soon.
 
You've changed the goalposts a bit again. Originally you showed all the items already in the ListView but you changed that in post #4 and I didn't notice that now you're adding the items as part of this operation. I guess that makes sense but it also means that you can change your approach a bit. As long as you don't add them to the ListView, you can create the ListViewItems on the secondary thread. That would speed up the overall operation.
Imports System.ComponentModel
Imports System.IO

Public Class Form1

    Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
        ListView1.Items.Clear()
        ImageList1.Images.Clear()

        Dim folderPath = CStr(e.Node.Tag)

        BackgroundWorker1.RunWorkerAsync(folderPath)
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Dim folderPath = CStr(e.Argument)
        Dim filePaths = Directory.GetFiles(folderPath)
        Dim upperBound = filePaths.GetUpperBound(0)
        Dim items(upperBound) As ListViewItem
        Dim images(upperBound) As Image

        For i = 0 To upperBound
            Dim filePath = filePaths(i)

            items(i) = New ListViewItem(Path.GetFileName(filePath), i) With {.Tag = filePath}
            images(i) = GetThumbnailImage(filePath)
        Next

        e.Result = New Tuple(Of ListViewItem(), Image())(items, images)
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
        Dim result = DirectCast(e.Result, Tuple(Of ListViewItem(), Image()))
        Dim items = result.Item1
        Dim images = result.Item2

        ImageList1.Images.AddRange(images)
        ListView1.Items.AddRange(items)
    End Sub

    Private Function GetThumbnailImage(filePath As String) As Image
        'TODO: Get the thumbnail image for the specified file.
    End Function

End Class
If you want to load the items in pages then you can do basically the same thing except you call ReportProgress repeatedly and load the items in the ProgressChanged event handler.
Imports System.ComponentModel
Imports System.IO

Public Class Form1

    Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
        ListView1.Items.Clear()
        ImageList1.Images.Clear()
        ProgressBar1.Value = 0

        Dim folderPath = CStr(e.Node.Tag)

        BackgroundWorker1.RunWorkerAsync(folderPath)
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Dim worker = DirectCast(sender, BackgroundWorker)
        Dim folderPath = CStr(e.Argument)
        Dim filePaths = Directory.GetFiles(folderPath)
        Dim upperBound = filePaths.GetUpperBound(0)
        Dim totalItemCount = filePaths.Length

        Const PAGE_SIZE As Integer = 10

        Dim items As New List(Of ListViewItem)(PAGE_SIZE)
        Dim images As New List(Of Image)(PAGE_SIZE)

        For i = 0 To upperBound
            Dim filePath = filePaths(i)

            items.Add(New ListViewItem(Path.GetFileName(filePath), i) With {.Tag = filePath})
            images.Add(GetThumbnailImage(filePath))

            Dim itemCount = items.Count

            'Check whether we have a full page.
            If itemCount = PAGE_SIZE Then
                Dim progress = 100 * itemCount \ totalItemCount

                worker.ReportProgress(progress,
                                      New Tuple(Of ListViewItem(), Image())(items.ToArray(),
                                                                            images.ToArray()))

                items.Clear()
                images.Clear()
            End If
        Next

        'Check whether we have a partial page.
        If items.Count <> 0 Then
            e.Result = New Tuple(Of ListViewItem(), Image())(items.ToArray(), images.ToArray())
        End If
    End Sub

    Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
        ProgressBar1.Value = e.ProgressPercentage

        With DirectCast(e.UserState, Tuple(Of ListViewItem(), Image()))
            LoadItems(.Item1, .Item2)
        End With
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
        ProgressBar1.Value = ProgressBar1.Maximum

        If e.Result IsNot Nothing Then
            With DirectCast(e.Result, Tuple(Of ListViewItem(), Image()))
                LoadItems(.Item1, .Item2)
            End With
        End If
    End Sub

    Private Function GetThumbnailImage(filePath As String) As Image
        'TODO: Get the thumbnail image for the specified file.
    End Function

    Private Sub LoadItems(items As ListViewItem(), images As Image())
        ImageList1.Images.AddRange(images)
        ListView1.Items.AddRange(items)
    End Sub

End Class
 
Thanks, this is working really well and I think I understand most of it. The only problem I'm having now is in case another folder is selected before the last is done loading... I'm trying to get CancelAsync() but it's not having it.

VB.NET:
         Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
        folderPath = CStr(e.Node.Tag)
        If BackgroundWorker1.IsBusy Then BackgroundWorker1.CancelAsync()
        LoadIcons()

    End Sub

I made folderPath public so that I could call LoadIcons again if I want to change the icon size.

VB.NET:
    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Dim worker = DirectCast(sender, BackgroundWorker)
        Dim folderPath = CStr(e.Argument)
        Dim filePaths = Directory.GetFiles(folderPath)
        Dim upperBound = filePaths.GetUpperBound(0)
        Dim totalItemCount = filePaths.Length

        Const PAGE_SIZE As Integer = 500

        Dim items As New List(Of ListViewItem)(PAGE_SIZE)
        Dim images As New List(Of Image)(PAGE_SIZE)

        For i = 0 To upperBound
            Dim filePath = filePaths(i)

            items.Add(New ListViewItem(Path.GetFileName(filePath), i) With {.Tag = filePath})
            images.Add(GetThumbnailImage(filePath))

            Dim itemCount = items.Count

            If BackgroundWorker1.CancellationPending = True Then
                e.Cancel = True
                Exit For
            End If
            'Check whether we have a full page.

            If itemCount = PAGE_SIZE Then
                Dim progress = 100 * itemCount \ totalItemCount

                worker.ReportProgress(progress,
                                      New Tuple(Of ListViewItem(), Image())(items.ToArray(),
                                                                            images.ToArray()))

                items.Clear()
                images.Clear()
            End If
        Next

        'Check whether we have a partial page.
        If items.Count <> 0 Then
            e.Result = New Tuple(Of ListViewItem(), Image())(items.ToArray(), images.ToArray())
        End If
    End Sub

^The cancellation

But I still get the error from the background worker already being busy. This will particularly be a problem later on because I wish to add a tag search feature so the entire list may be changing very dynamically. I can handle that part, I can see where you have loaded the filenames and I can just add my own list there. But I do need to figure out how to cancel and restart the background worker.

I've tried using a while loop to just wait until the cancellation is no longer pending and just for the heck of it I tested out sleeping the thread just being calling LoadIcons just to give it a moment to finish up. But that didn't work. So I think I'm missing something here. I cannot find any clues in the MSDN example, I don't see what it's doing that I am not aside from reporting the cancellation to the user.
 
When you call CancelAsync, it doesn't actually cancel anything, but rather just requests a cancellation. It's up to your code to check to see whether a cancellation has been requested and abort what it's doing if it has. That may take some time, depending on how you write the code. CancelAsync returns immediately, so you can't simply start a new task at that point. If you do cancel the task then, in the RunWorkerCompleted event handler, the e.Cancelled property will be True. You can test that property and, if it's True, you know that the last task was cancelled, which only happens if you want to start another task, so you can start that other task there, e.g.
If e.Cancelled Then
    folderPath = CStr(TreeView1.SelectedNode.Tag)
    LoadIcons()
End If
 
Brilliant! That works perfectly, I would not have thought of that... Thank you.

VB.NET:
    Private Sub TreeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Handles TreeView1.AfterSelect
        folderPath = CStr(e.Node.Tag)
        If BackgroundWorker1.IsBusy Then
            BackgroundWorker1.CancelAsync()
        Else
            LoadIcons()
        End If
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
        ProgressBar1.Value = ProgressBar1.Maximum

      
        If e.Cancelled Then
            folderPath = CStr(TreeView1.SelectedNode.Tag)
            LoadIcons()
        ElseIf e.Result IsNot Nothing Then
            With DirectCast(e.Result, Tuple(Of ListViewItem(), Image()))
                LoadItems(.Item1, .Item2)
            End With
        End If
    End Sub

I've got this working about 100 times better than I would have expected at this point. I really appreciate your help and patience!
 
Ok, actually I am going to be a huge pain in the butt and say I have one more little problem that I really can't figure out without breaking other stuff.

I really like how it is loading in pages but I don't fully understand the use of Tuple and how you are relating the items to the images. While I like the way the images load I did still want to populate the listview first then start loading the thumbnails. When I run into a folder with something like a bunch of PDFs, it takes so long to load the thumbs I feel like the priority should first just be to show the files. I can't manage to do this without breaking the way it loads in pages. Is there an easy way to add this modification with how it is working now?
 
"Tuple" is a mathematical term for something that contains multiple items. It's the general term for a the family including the double, triple, quadruple, quintuple, sextuple, septuple, octuple, etc. As you can see, from 5 items onwards, the suffix is "tuple" in each case, which is where the family name comes from. The Tuple classes in .NET are just a way to group an arbitrary number of items together in an ad hoc fashion. If you want to group five items together of types String, Integer, String, DataTable and Double then you can create a Tuple(Of String, Integer, String, DataTable, Double) to do it.

As for the rest, I've written quite a bit of code there that demonstrates all the principles you need. If you want to implement those principles in a different way then you already have all the tools to do so.
 

Latest posts

Back
Top