Using the CLR 2.0 Virtual List View

I started writing an article about the bugs with the new ListView in virtual mode, but decided that I should first write one about how a virtual ListView actually works. The next post will be about two serious bugs and a simple solution to them.

 

One of my favorite new features in Whidbey* WinForms is the ability to have virtual ListView controls. The first time I used the feature heavily was when I made an application that displayed large amount of information (log output). Just adding the log lines to the ListView was out of the question for a few important reasons.

1. The first reason is the basic fact that adding ListViewItem instances to the ListView means duplicating at least some of the information – and when we are talking about millions of lines, that’s something you want to avoid.

2. The second is that when you have duplicated data structures, you need to keep them synced in some way. In the case of the above application, there were some filtering capabilities that allowed the user to change which items were displayed. In those cases, the ListView would have had to be regenerated every time (or synced in some other manner) to make sure it displays the correct information.

Virtual ListView limitations

A few things to note when using Virtual ListView controls. It does not support all ListView features. Examples are:

1. Tile View – if you are in Tile view in the ListView, it will change to LargeIcons instead.

2. SelectedItems/CheckedItems – these properties are not supported. Instead, use the SelectedIndices/ChecjedIndices properties to figure out which items exist in those collections.

3. Changing amount of sub items in a ListViewItem instance – when creating ListViewItem instances, you need to make sure that it has the appropriate amount of ListViewSubItem instances in it. With regular list view, you can omit some of them and they will appear empty in the ListView.

4. ArrangeIcons() – this method is not supported.

5. Groups – you cannot use groups in a virtual ListView.

6. Positioning of items – you cannot position ListViewItems in any of the icon modes.

7. Automatic sorting – you cannot use the Sort() method on the ListView or any associated operations.

 

This is a partial list, but should cover most of the important things.

Basic virtual ListView

With Whidbey and the virtual ListView. The idea is pretty simple – instead of you adding items to the list, you tell the list how many items it has, and it calls back to you when it wants them. In its most basic form, you need only to do the following things when you want to create a virtual ListView:

1. Set the VirtualMode property of the ListView to true.

2. Implement the RetrieveVirtualItem event from which you return a ListViewItem for the supplied index.

3. Set the size of the list by using the VirtualListSize property.

 

As an example, we will create a ListView that shows information about dates between 01/18/1952 to 04/14/2006 – each line in the ListView will show the date in the single column we have.

For starters, we will create a form and add a ListView control to it. We will set the VirtualMode property of the form to true and implement the RetrieveVirtualItem event of the ListView, the Load event of the Form and we will also add some members that will represent our start and end time:

 

private DateTime m_start = new DateTime(1952, 1, 18);

private DateTime m_end = new DateTime(2006, 4, 14);

private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)

{

    e.Item = GetListItem(e.ItemIndex);

}

private ListViewItem GetListItem(int i)

{

    DateTime itemTime = m_start + TimeSpan.FromDays(i);

    ListViewItem lvi = new ListViewItem(itemTime.ToShortDateString());

    lvi.Tag = itemTime;

    return lvi;

}

private void Form1_Load(object sender, EventArgs e)

{

    TimeSpan span = m_end - m_start;

    listView1.VirtualListSize = (int)span.TotalDays;

}

 

How would it have worked with a regular ListView

With a regular ListView, we would have had to add 19,810 items to the list, take up precious memory, not to even talk about the fact that the amount of time it would take to add all those items, making our UI startup sluggish. In contrast, going the virtual way makes that whole process take virtually no time at all (pun intended). We do get a potential perf hit, depending on the usage scenario, but that perf hit is amortized over time and since a ListView is an object that interacts with a user, it should be near unnoticeable.

Adding some makeup

The ListViewItem objects also support coloring – that can be leveraged in virtual ListViews as well. In our example, when the user double clicks an item in the list, we will give all the other items that are the same day of week a cyan background. For that, we need to make changes to our GetListItem helper method, add another member to the class and add a DoubleClick event to the ListView:

private DayOfWeek? m_dayToHighlight;

private ListViewItem GetListItem(int i)

{

    DateTime itemTime = m_start + TimeSpan.FromDays(i);

    ListViewItem lvi = new ListViewItem(itemTime.ToShortDateString());

    lvi.Tag = itemTime;

    if (m_dayToHighlight != null && itemTime.DayOfWeek == m_dayToHighlight.Value)

    {

        lvi.BackColor = Color.Cyan;

    }

    return lvi;

}

private void listView1_DoubleClick(object sender, EventArgs e)

{

    if (listView1.SelectedIndices.Count == 1)

    {

        // we cant access the ListView.SelectedItems collection when in

        // virtual mode. So we go through the Items collection and the

        // SelectedIndices collection.

        ListViewItem item = listView1.Items[listView1.SelectedIndices[0]];

        DateTime itemTime = (DateTime)item.Tag;

        m_dayToHighlight = itemTime.DayOfWeek;

        listView1.Refresh();

    }

}

How would it have worked with a regular ListView

To get the same functionality with a regular ListView, we would have had to go over all the items in the list and re-sync them with the operation we just did (which means going over each one of the 20,000 items and made sure that the correct color is there).

Optimizing the fetching of items

The RetrieveVirtualItem event will execute more than you think. Excessively so. On average, the event will fire four times for each item visible in a ListView. For the selected items, it may be called more than that. So if you have animation turned in and you page-down the list, you can imagine how many times the event will be fired.

For this reason, the ListView control has an event called CacheVirtualItems which tells you which range of items the control is working on.

To have this functionality we will implement said event, add two members (array of items and an integer that represents what area that array caches) and will change the implementation of the RetrieveVirtualItem event.

 

private ListViewItem[] m_cache;

private int m_firstItem;

 

private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)

{

    // If we have the item cached, return it. Otherwise, recreate it.

    if (m_cache != null &&

        e.ItemIndex >= m_firstItem &&

        e.ItemIndex < m_firstItem + m_cache.Length)

    {

        e.Item = m_cache[e.ItemIndex - m_firstItem];

    }

    else

    {

        e.Item = GetListItem(e.ItemIndex);

    }

}

 

private void listView1_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)

{

    // Only recreate the cache if we need to.

    if (m_cache != null &&

    e.StartIndex >= m_firstItem &&

        e.EndIndex <= m_firstItem + m_cache.Length)

        return;

    m_firstItem = e.StartIndex;

    int length = e.EndIndex - e.StartIndex + 1;

    m_cache = new ListViewItem[length];

    for (int i = 0; i < m_cache.Length; i++)

    {

        m_cache[i] = GetListItem(m_firstItem + i);

    }

}

 

One last thing we need to do though is to invalidate the cache when something changes in our data. For this, we will add a ClearCache method and call it when our data changes:

 

private void ClearCache()

{

    m_cache = null;

}

 

private void listView1_DoubleClick(object sender, EventArgs e)

{

    if (listView1.SelectedIndices.Count == 1)

    {

        // we cant access the ListView.SelectedItems collection when in

        // virtual mode. So we go through the Items collection and the

        // SelectedIndices collection.

        ListViewItem item = listView1.Items[listView1.SelectedIndices[0]];

        DateTime itemTime = (DateTime)item.Tag;

        m_dayToHighlight = itemTime.DayOfWeek;

        ClearCache();

        listView1.Refresh();

    }

}

 

 

On slow machines, even with simple examples like the one I just gave, you can see an actual perf improvement when making use of the caching event.

 

Conclusion

When you have a lot of information to display or when the information the list is based on changes a lot, it may be worth it to use the virtual ListView mode to make your life easier. In the next article I will discuss two nasty bugs with the virtual mode that are pretty easy to solve.

 

* I wonder if we are at that magical period of time when saying “Whidbey” makes you cool. Kind’a like saying “Chicago” did back when Windows 95 came out and “Whistler” back when XP came out.

 

Edits: Ken Argo found a bug in the CacheVirtualItems event I implemented - the number was off-by-one and would have caused an unnecessery perf impact. Fixed it in the sample. Thanks Ken!