Visual Basic .NET And Virtual ListView Glitches

I like virtual ListViews – drop one on a form, set the virtual flag, the length of data, and off we go – no filling needed.

Well, not exactly. It turns out that ListView has some quirks, as I found out today while doing some “obvious” coding.

Virtual ListViews allow you to create a grid of data that doesn’t need to exist anywhere – calculate it when needes, generate it from other data, etc – but the key is its an “on the fly” control that gives you better performance, especially if you have large numbers of rows to view (note I’m talking about the ListView with columns, which is the ‘View.Details’ setting in the properties).

Trying it out is easy – add a ListView to a form, and at a minimum you’ll need this somewhere:

ListView1.VirtualMode = True
ListView1.View = View.Details ' rows and columns view mode
ListView1.VirtualListSize = 100 ' your data should tell it how big it is

And also a RetrieveVirtualItem handler for the ListView:

Private Sub ListView1_RetrieveVirtualItem(ByVal sender As System.Object, _
  ByVal e As System.Windows.Forms.RetrieveVirtualItemEventArgs) _
  Handles ListView1.RetrieveVirtualItem
    Dim item = New ListViewItem("Col 1,Row " & e.ItemIndex.ToString)
    e.Item = item
End Sub

Oh, and you’ll need a column for this – I just go into the Columns entry in the ListView property, click on the ‘…’ button, then ‘Add’.

Run it, and you’ll get 100 entries displayed, yet there’s no actual data list maintained inside (except the item object you created and passed to the ListView, of course). We use the e.ItemIndex value to get the virtual row’s index, and then just display it as part of the caption. Passing the ListViewItem object item to e.Item is what gives the ListView the data to show.

Most examples online give you something like this, but they leave out the first problem you’ll encounter next – moving from this to multiple columns.

The answer to that is SubItems, and the key to remember is that while the first column is a ListViewItem, the remaining columns are added to the first as SubItems. With this in mind, here’s the code for three columns (don’t forget to add two more columns to the ListView or you won’t see anything):

Private Sub ListView1_RetrieveVirtualItem(ByVal sender As System.Object, ByVal e As System.Windows.Forms.RetrieveVirtualItemEventArgs) Handles ListView1.RetrieveVirtualItem
  Dim item = New ListViewItem("Col 1,Row " & e.ItemIndex.ToString)
  item.SubItems.Add("Col 2,Row " & e.ItemIndex.ToString)
  item.SubItems.Add("Col 3,Row " & e.ItemIndex.ToString)
  e.Item = item
End Sub

Now you’ll get 100 rows, and 3 columns, with each entry describing the column and row it is in.

View the display, and everything lines up – but a bit confusing internally, where the first SubItem you added is actually the second column’s data! The answer of course is that creating the first item takes care of the first SubItem as well, but it still seems a bit strange…

At this point, life is good. You can build a virtual ListView, add features, and put columns in it like mad, which brings us to problem two – you ALWAYS need at least as many SubItems as columns.

Add five columns, yet add only three SubItems (for a total of four – don’t forget that first item), and you’ll get a runtime error when you display the list.

Worse still, this error is only if you compile for ‘All CPUs’ in the debug/release configurations – do it for ‘x86’ and you’ll get weird display glitches, with no runtime crash at all. Needless to say, this will result in some really annoying debugging, as I found out. At one point, I was even implementing OwnerDraw handlers in case I had screwed up the display! But in fact under x86 configuration it won’t work, but it won’t error out either (at least under .NET 2.0 and 3.5 in my testing).

But even if it did error out, runtime errors are not something to tolerate in production code – so what do we do?

One option is an assert at the end of the RetrieveVirtualItem() handler:

Debug.Assert(item.SubItems.Count >= DirectCast(sender, ListView).Columns.Count, "Not enough subitems - poor drawing ahead...")

The Sender (our ListView) is checked for the column count, compared to our items count, and a warning pops up if there’s not enough (note that too many items isn’t a problem, only too few – of course, there’s no columns to display them in…)

And while a useful check, when it fires the program gets seriously twonky (it’s in a display handler after all). Plus it still doesn’t fix the problem, only alerts us to it.

So how about some protection safety code instead:

Private Sub ListView1_RetrieveVirtualItem(ByVal sender As System.Object, ByVal e As System.Windows.Forms.RetrieveVirtualItemEventArgs) Handles ListView1.RetrieveVirtualItem
  Dim item = New ListViewItem("Col 1,Row " & e.ItemIndex.ToString)
  item.SubItems.Add("Col 2,Row " & e.ItemIndex.ToString)
  item.SubItems.Add("Col 3,Row " & e.ItemIndex.ToString)
  ' add empty items until we match the columns
  While (item.SubItems.Count < DirectCast(sender, ListView).Columns.Count)
    item.SubItems.Add("")
  End While
  e.Item = item
End Sub

The while loop only adds empty SubItems if we’ve forgotten to, resulting in a “safe” virtual display, and adds a catch for errors. While some may feel good code shouldn’t need this kind of patching, I know what maintenance is like: Software you thoroughly understood six months ago has achieved near-hieroglyphic status today, and any bit of safety code like this is much more welcome that tracing down a weird display glitch!

So using this code, you should never encounter these problems with ListViews, and the result is a very handy display for large amounts of data. In fact, once you start using it, you’ll likely find it so flexible that you’ll reach for it instead of a ListBox for almost any task!

One thought on “Visual Basic .NET And Virtual ListView Glitches