I've written a FileDownloader class to download files via a separate thread. The progress of each download is shown in a ListView with VirtualMode enabled.

Everything works perfectly fine, the only problem is that calling ListView.RedrawItems or ListView.Invalidate() in a Timer every second, to show the progress changes in the ListView, is very inefficient. It results in constant 2% CPU usage.

vb.net Code:
  1. ' Inefficient. Constant 2% cpu usage. I want to get rid of this timer.
  2. Private Sub TimerUpdateListView_Tick(sender As System.Object, e As System.EventArgs) Handles TimerUpdateListView.Tick
  3.     ListViewDownloads.RedrawItems(0, FileDownloadList.Count - 1, True)
  4. End Sub

I found out that calling ListView.RedrawItems for every item separately is much more efficient.

vb.net Code:
  1. ' Runs on UI thread. Fires every second for each download
  2. Private Sub DownloadProgressChanged(sender As Object, e As DownloadProgressChangedEventArgs)
  3.     ' e.Item is the associated FileDownloadItem instance
  4.     e.Item.Size = e.Size
  5.     e.Item.TransferRate = e.TransferRate
  6.  
  7.     ' I want to call RedrawItems for every item separately here instead of in a Timer, because it's more efficient,
  8.     ' but how do I know which index to use here when items are being deleted from the background list?
  9.     ListViewDownloads.RedrawItems(Index, Index, True)
  10. End Sub

My problem is that I can't pass the index of the item to the FileDownloader class and then use it in the DownloadProgressChanged event, because when a download is finished, it is removed from the background list. This changes the indexes of the other items in the background list, making the index passed to the FileDownloader class invalid.

It's probably something easy again that I'm not seeing, which is my biggest problem lately...

Here's a simplified version of my code to show what I'm trying to do.

vb.net Code:
  1. ' Background list for the Virtual ListView
  2.     Private FileDownloadList As New List(Of FileDownloadItem)
  3.  
  4.     Private Sub ButtonDownload_Click(sender As System.Object, e As System.EventArgs) Handles ButtonDownload.Click
  5.         ' Create new instance of FileDownloadItem
  6.         Dim fdi As New FileDownloadItem
  7.         fdi.Filename = "File.pdf"
  8.         fdi.Size = 0
  9.         fdi.TransferRate = 0
  10.  
  11.         ' Add it to the background list
  12.         FileDownloadList.Add(fdi)
  13.  
  14.         ' Let the ListView know how many items we have in our background list and make the changes visible
  15.         ListViewDownloads.VirtualListSize = FileDownloadList.Count
  16.         ListViewDownloads.Invalidate()
  17.  
  18.         ' Create instance of FileDownloader class and pass the FileDownloadItem as the last parameter,
  19.         ' so we can use it when mashalling back the UI thread
  20.         Dim downloader As New FileDownloader("http://website.com/file.pdf", "C:\file.pdf", fdi)
  21.         AddHandler downloader.DownloadProgressChanged, AddressOf DownloadProgressChanged
  22.         AddHandler downloader.DownloadCompleted, AddressOf DownloadCompleted
  23.         downloader.SynchronizingObject = Me
  24.         downloader.Start()
  25.     End Sub
  26.  
  27.     ' Interval = 1000
  28.     ' Inefficient. Constant 2% cpu usage. I want to get rid of this timer.
  29.     Private Sub TimerUpdateListView_Tick(sender As System.Object, e As System.EventArgs) Handles TimerUpdateListView.Tick
  30.         ListViewDownloads.RedrawItems(0, FileDownloadList.Count - 1, True)
  31.     End Sub
  32.  
  33.     ' Runs on UI thread. Fires every second for each download
  34.     Private Sub DownloadProgressChanged(sender As Object, e As DownloadProgressChangedEventArgs)
  35.         ' e.Item is the associated FileDownloadItem instance
  36.         e.Item.Size = e.Size
  37.         e.Item.TransferRate = e.TransferRate
  38.  
  39.         ' I want to call RedrawItems for every item separately here instead of in a Timer, because it's more efficient,
  40.         ' but how do I know which index to use here when items are being deleted from the background list?
  41.         ListViewDownloads.RedrawItems(Index, Index, True)
  42.     End Sub
  43.  
  44.     ' Runs on UI thread
  45.     Private Sub DownloadCompleted(sender As Object, e As DownloadCompletedEventArgs)
  46.         Dim downloader As FileDownloader = DirectCast(sender, FileDownloader)
  47.         RemoveHandler downloader.DownloadProgressChanged, AddressOf DownloadProgressChanged
  48.         RemoveHandler downloader.DownloadCompleted, AddressOf DownloadCompleted
  49.  
  50.         ' Delete item from the background list.
  51.         FileDownloadList.Remove(e.Item)
  52.     End Sub
  53.  
  54.     ' Show items when requested
  55.     Private Sub ListViewDownloads_RetrieveVirtualItem(sender As Object, e As System.Windows.Forms.RetrieveVirtualItemEventArgs) Handles ListViewDownloads.RetrieveVirtualItem
  56.         Dim lvw As New ListViewItem
  57.         lvw.Text = FileDownloadList(e.ItemIndex).Filename
  58.         lvw.SubItems.Add(FileDownloadList(e.ItemIndex).Size.ToString)
  59.         lvw.SubItems.Add(FileDownloadList(e.ItemIndex).TransferRate.ToString)
  60.         e.Item = lvw
  61.     End Sub