Ross from Brooklin
Ross from Brooklin

Reputation: 303

How can I speed up VB copy

I'm running the following loop successfully when the number of items is low. However, when run against a larger list on the ListView, it seems to be taking way too long. I tested it with a list of 8,700 files and it took about two hours to complete. Is there something I can do to speed this up? I guess that removing the check for the Cancel button would help but I would like to keep that there for usability. As I've mentioned in earlier posts, I'm pretty new to Visual Basic so please provide lots of explanation with your suggestions. Thanks. Here's the code:

    For i As Integer = 0 To m_CountTo

        ' Has the background worker be told to stop?
        If BackgroundWorker1.CancellationPending Then
            ' Set Cancel to True
            e.Cancel = True
            Exit For
        End If

        'Select the row from the LVFiles ListView, then move the first column (0) into strSourceFilePath and the last
        ' column (3) into strDestFilePath. Execute the CopyFile method to copy the file.

        LVFiles.Items(i).Selected = True
        strSourceFilePath = LVFiles.SelectedItems(i).SubItems(0).Text
        strDestFilePath = LVFiles.SelectedItems(i).SubItems(3).Text
        My.Computer.FileSystem.CopyFile(strSourceFilePath, strDestFilePath, overwrite:=False)

        ' Report The progress of the Background Worker.
        BackgroundWorker1.ReportProgress(CInt((i / m_CountTo) * 100))
        ' Me.LabelStatus.Text = FormatPercent((i + 1) / (intLVIndex + 1), 2) ' Show Percentage in Label
        SetLabelText_ThreadSafe(Me.LabelStatus, FormatPercent(i / m_CountTo, 2))

    Next

Upvotes: 0

Views: 319

Answers (1)

keco
keco

Reputation: 146

The Backgroundworker encapsulates a new thread. You cannot directly access controls that are created in another thread. If you do you will get an InvalidOperationException because of a cross-thread operation. The Backgroundworker however offers some functionality to share data (or access to controls) between threads. You should use them.

    Private Sub StartBGW_Click(sender As Object, e As EventArgs) Handles StartBGW.Click
        Dim dict As New Dictionary(Of String, String)
        For i As Integer = 0 To m_CountTo
            dict.Add(Me.LVFiles.Items(i).SubItems(0).Text,
                     Me.LVFiles.Items(i).SubItems(3).Text)
        Next

        Me.BackgroundWorker1.RunWorkerAsync(dict)
    End Sub

First we prepare a dictionary that contains the source as Key and the target as Value. This object is given to the BackgroundWorker as a parameter.

Now comes the essential part:

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Dim counter As Integer = -1
        Dim dict = DirectCast(e.Argument, Dictionary(Of String, String))
        For Each kvp In dict
            counter += 1

            ' Has the background worker be told to stop?
            If Me.BackgroundWorker1.CancellationPending Then
                ' Set Cancel to True
                e.Cancel = True
                Exit For
            End If

           'Select the row from the LVFiles ListView, then move the first column (0) into strSourceFilePath and the last
           ' column (3) into strDestFilePath. Execute the CopyFile method to copy the file.

            My.Computer.FileSystem.CopyFile(kvp.Key, kvp.Value, overwrite:=False)

            ' Report The progress of the Background Worker.
            Me.BackgroundWorker1.ReportProgress(CInt((counter / m_CountTo) * 100), counter)
        Next
    End Sub

We don't access the ListView anymore. Instead we use the dictionary that is given to us as a parameter through e.Argument. Theres also a slight difference in the BackgroundWorker1.ReportsProgress line. There's a second parameter I have used to pass the current index to the ProgressChanged event which can be obtained via e.UserState.

    Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
        Me.LVFiles.Items(Convert.ToInt32(e.UserState)).Selected = True
        Me.LabelStatus.Text = e.ProgressPercentage.ToString
    End Sub

This event is designed to be raised with a SynchronizationContext of the calling thread, in this case the UI thread. Here we can safely access any control and update them. The index is passed as e.UserState, so we can access the relevant item and set their Selected property to true.


The biggest improvement comes from the change of Me.LVFiles.SelectedItems(i).SubItems(0).Text to Me.LVFiles.Items(i).SubItems(0).Text. I'm not a professional, but it seems that SelectedItems isn't a real list. Instead it iterates through every item using the SendMessage API until the desired index is reached. This is why it takes longer the higher your index is. Everytime it starts with the first item and iterates through them. Lot of operations.

The second improvement is the separation of code that access UI controls. It's all done in one method now. More clear and readable.

Update: @Enigmativity mentioned that SelectedListViewItemCollection implements IList and therefore is a real list. Even though it has no underlying list containing all selected items like you have in ListViewItemCollection. My point was to say, that accessing a single element is more complicated.

Upvotes: 1

Related Questions