Did you ever ask yourself during coding how far you can get with simple constructs and when it is time to use the more advanced but also more complex constructs? When you take the next step it can happen that you discover that the more “complex” solution is even more simple! I had such an aha moment with the Windows Forms ListView. I had one in detailed mode with several thousand rows which did show up very slowly because of the huge number of ListViewItem objects inside it.
Nothing fancy but for 20K rows it did take about 5s until something was displayed. The code to display items in list view is straightforward:
class AccountInfo
{
public string AccountType
{
get;
set;
}
public int AccountBalance
{
get;
set;
}
public int BuyRating
{
get;
set;
}
}
public partial class cAccounting : Form
{
List<AccountInfo> myInfos = new List<AccountInfo>();
public cAccounting()
{
InitializeComponent();
for (int i = 0; i < 20000; i++)
{
myInfos.Add(new AccountInfo
{
AccountType = i % 3 == 0 ? "Good" : "Bad",
AccountBalance = i,
BuyRating = i*2
});
}
ShowItems(myInfos);
}
void ShowItems(List<AccountInfo> infos)
{
cList.SuspendLayout();
foreach (var acc in infos)
{
cList.Items.Add( new ListViewItem(new string [] { acc.AccountType, acc.AccountBalance.ToString(), acc.BuyRating.ToString() }));
}
cList.ResumeLayout();
}
So far so good. Now I wanted to make it faster. Let´s see how things do change when we make the ListView virtual. This little change does cause the list view to fetch only the visible items. Only visible ListViewItems are fetched on demand. If you have 10 million rows but display only 100 of them then only 100 ListViewItem objects will be used.

We no longer have the Items collection which would throw exceptions when we would try to use it. The SelectedItems collection is also gone. Instead the SelectedIndices is your friend. Well nearly I found that my items were deselected without beeing notified by the ItemSelectionChanged event so I stored the last selected item index by myself when the SelectedIndicies collection was not empty.
But back to our virtual list view: We only need to register to the RetrieveVirtualItem event and set the VirtualListSize property to the number of items our ListView will contain. That´s all.
The contract is that the event tells you for which ItemIndex in the list view wants to draw a ListViewItem and you set the Item property correctly.
private void ShowItemsVirtual(List<AccountInfo> infos)
{
cList.VirtualListSize = infos.Count; // Set number of items in list view
}
private void cList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
var acc = myInfos[e.ItemIndex];
// hand out new item
e.Item = new ListViewItem(
new string []
{ acc.AccountType, acc.AccountBalance.ToString(), acc.BuyRating.ToString() })
{ Tag = acc }; // Set Tag object property to our actual AccountInfo object
}
That was not hard. And we get a display speed increase of over a factor 5 with little effort. The startup time has dropped from 5s to below 1s. But the best thing is that we no longer have set the state of our ListViewItems when we want to e.g. color them. Previously we needed to traverse the whole Items collection and set the e.g. BackColor property for the required ListViewItems by hand. Now we can store the state in one variable and every time the list view is redrawn we hand out ListViewItem objects with the updated state. Bye bye list traversing and setting properties.
To update the color of every second row we need to insert only one additonal line in our RetrieveVirtualItem callback:
private void cList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
var acc = myInfos[e.ItemIndex];
e.Item = new ListViewItem(
new string []
{ acc.AccountType, acc.AccountBalance.ToString(), acc.BuyRating.ToString() })
{ Tag = acc,
BackColor = e.ItemIndex % 2 == 0 ? Color.Aqua : Color.White
};
}
To display the colored items in the current view we need to call Refresh to force the list view to fetch our new ListViewItems. We have full control which item is on which position rendered. Now it is time for sorting by different columns. In virtual mode the usual ListViewItem sorter is not supported so we have to roll our own. I find the code below actually easier to write because I do know that the data is stored in List<AccountInfo> myInfos and we only need to sort the list by the current sort column ascending or descending order.
// hold state for each colum which sort order should be used
Dictionary<int,SortOrder> mySortOrderMap = new Dictionary<int,SortOrder>
{
{0, SortOrder.None },
{1, SortOrder.None },
{2, SortOrder.None }
};
// define comparer for each column
Dictionary<int, Comparison<AccountInfo>> myComparers = new Dictionary<int, Comparison<AccountInfo>>
{
{0, (a,b) => a.AccountType.CompareTo(b.AccountType)},
{1, (a,b) => a.AccountBalance.CompareTo(b.AccountBalance)},
{2, (a,b) => a.BuyRating.CompareTo(b.BuyRating)}
};
// State transitions from one sort order to another
Dictionary<SortOrder, SortOrder> myToggle = new Dictionary<SortOrder, SortOrder>
{
{SortOrder.None, SortOrder.Ascending },
{SortOrder.Ascending, SortOrder.Descending},
{SortOrder.Descending, SortOrder.Ascending}
};
private void cList_ColumnClick(object sender, ColumnClickEventArgs e)
{
var newSortOrder = myToggle[mySortOrderMap[e.Column]];
mySortOrderMap[e.Column] = newSortOrder; // Store sort order for current column
SortBy(newSortOrder, myComparers[e.Column]); // Do the actual sorting
}
void SortBy(SortOrder order, Comparison<AccountInfo> comparer)
{
myInfos.Sort((a, b) =>
{
int lret = comparer(a, b); // Do the actual comparison
if (order == SortOrder.Descending) // reverse when necessary
{
lret *= -1;
}
return lret;
});
cList.Refresh();
}
This was much easier than I thought and what I especially like about virtual list views is that I do not have to worry about the state of our rendered ListViewItems. Instead the “factory” event will provide items with the newest state directly to the view once we invalidated the list view by calling Refresh.