Eliminating CLR bugs in the Whidbey ListView (Virtual mode)
I discussed using a ListView control in virtual mode in a previous post.
When using the ListView control in virtual mode, there are two bugs that can make working with it a bit of a pain. At the end of this post, I have placed a class called SafeListView which solves these bugs transparently (so you can just use it instead of the Whidbey ListView).
First bug – ListViewItems with text length equaling 260 characters
The first bug is a bit of a hassle to find, and is very easy to fix. To repro it, create a form with a virtual list view on it – make sure the ListView has one column and that its View property is set to Detail. Then add the following code to the RetrieveVirtualItem event:
private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
ListViewItem item;
if (e.ItemIndex < VirtualListSize - 1)
{
item = new ListViewItem("Scroll down for boom");
}
else
{
item = new ListViewItem(new String('a', 260));
}
e.Item = item;
}
Then, add to the forms Load event the following code:
listView1.VirtualListSize = 100;
Run the form and scroll the list view to the end. You should see a crash when you reach the last item.
What is happening?
Well, the WinForms code has a bug that causes an exception when an item text length is exactly 260 characters long (in virtual list mode). The solution is to, well, not add items with a length of 260. Anything above or below will do. It’s worth noting though that anything over 259 characters will be truncated by the list view.
Second bug – Changing VirtualListSize
In most cases, changing VirtualListSize should work without a problem, however, if you try to set the list size to a number that is smaller than that of the TopItem index, you will again get a crash.
The TopItem property holds the first visible ListViewItem in the list view. So if you have 100 items in a list view which can show only 10 and you are focused on the last item in the list (the one at the bottom of your ListView), your TopItem should be the 90th item in the list. If at that stage the number of items in the list halved (by changing VirtualListSize to 50), the code would throw a cryptic exception.
To see the bug happening, create a form and place a list view and a button on it. Change VirtualMode equals to true. Make sure you add one column and change the View to be Detail. Also place a button on the form.
Implement the RetrieveVirtualItem event on the ListView, the Load event on the form and the Click event on the button.
private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
ListViewItem item;
if (e.ItemIndex == listView1.VirtualListSize - 1)
{
item = new ListViewItem("Click on this item and then click the remove button.");
}
else
{
item = new ListViewItem("Scroll down!");
}
e.Item = item;
}
private void ChangingSizeBug_Load(object sender, EventArgs e)
{
listView1.VirtualListSize = 100;
}
private void button1_Click(object sender, EventArgs e)
{
listView1.VirtualListSize /= 2;
}
If you now run the form, scroll down to the bottom and click the button – you should see an exception being raised. The exception will be thrown from inside the set handler of the VirtualListSize property and is not your fault (so stop beating yourself about it). The problem is that there’s a bug with the handling of the TopItem property inside the ListView code. To solve that, you need to set the TopItem to an item that is in the new range.
SafeListView
Enters SafeListView that transparently solves this problem – it works exactly the same way as the ListView.
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
namespace TestListView
{
class SafeListView : ListView
{
/// <summary>
/// Called when the control need a virtual item.
/// </summary>
/// <param name="e">The event object the user needs to fill up.</param>
protected override void OnRetrieveVirtualItem(RetrieveVirtualItemEventArgs e)
{
// Get the list view item from the user.
base.OnRetrieveVirtualItem(e);
if (e.Item != null)
{
// Go over all the sub items in the list view
foreach (ListViewItem.ListViewSubItem subItem in e.Item.SubItems)
{
// If an items text is 260 characters long, add a space so it does
// not crash the program.
if (subItem.Text.Length == 260)
{
subItem.Text = subItem.Text + " ";
}
}
}
}
/// <summary>
/// The size of the VirtualListSize
/// </summary>
public new int VirtualListSize
{
get { return base.VirtualListSize; }
set
{
// If the new size is smaller than the Index of TopItem, we need to make
// sure the new TopItem is set to something smaller.
if (VirtualMode &&
View == View.Details &&
TopItem != null &&
value > 0 &&
TopItem.Index > value - 1)
{
TopItem = Items[value - 1];
}
base.VirtualListSize = value;
}
}
}
}
This new control addresses the two problems described in the article and can be used in your form. You should not need to change anything on your form – you can keep using your ListView as you did before and it will work transparently*.
* So how transparent is transparent… Note that the VirtualListSize is not overridden – it can’t be because it’s non virtual. This should be okay in most cases, however, if you are ever to access your SafeListView through a base class and call into VirtualListSize, you will not get the safe method called. So be advised.
Comments
Anonymous
April 26, 2006
The comment has been removedAnonymous
June 22, 2006
This seams to work:
private void SetVirtualListSize(int size)
{
listView.VirtualListSize = 0;
listView.VirtualListSize = size;
}Anonymous
August 16, 2006
I noticed the ListView continues to workfine if I ignored the exception it throws ("Changing VirtualListSize"):
try
{
_listView.VirtualListSize = _items.Count;
}
catch(ArgumentOutOfRangeException e)
{
Console.WriteLine("Suppressed exception.");
}
Don't know if this has any side effects, but it seems to work for me. BTW, does anyone know if there is a list with bugs like these? Could have saved me a lot of time ...Anonymous
June 25, 2007
A workaround: Just before setting the VirtualListSize, change the View Mode to something like 'List'. After that, change it back to 'Detials': clientsList.View = View.List; clientsList.VirtualListSize = clientsData.Rows.Count; clientsList.View = View.Details;Anonymous
July 19, 2007
Thanks a lot for this. One thing I noticed is that inside VirtualListSize you need to switch the expressions TopList != null and value > 0 because if you have no items in the list i.e, setting the size to 0, TopList tries and retrieves item[0] and fails.