Virtual ListView crashes on VirtualListSize
Example:- listview with 20000 virtual items
-RetrieveVirtualItem() feeds some dynamically created items
- scroll to the end of the list
- set VirtualListSize to 200 (via a button click for instance)
You'll then crash internally with an ArgumentOutOfRangeException exception (here at index 200). Nothing helps to avoid this. Really odd. The crash happens somewhere in ListViewItemCollection.get_Item(). Looks like something isn't aware that things are virtualized...
Am I doing something wrong?
Or is this another listview bug?
The VB.NET source of the test app can be downloaded here:
http://www.hotpixel.net/tmp/VirtLstViewBug.zip
Thanks for reading!
[913 byte] By [
maakus] at [2007-12-17]
Yes it is a bug. It's sort of funny that even in the modern era of security-aware code that off by 1 errors still occur. Here is the problem. If you look at the code for setting VirtualListSize it follows this algorithm:
If Value < 0
Error
If (NewValue != CurrentValue)
If In Details Mode And Created And ...
Get the index of the first item displayed in the control (not necessarily the first item in the list)
Set the new virtual list size and send a message to notify the underlying control
If we have the index of the first item
Find the new top item in the list
Set TopItem to the new top item in the list
End If
End If
The problem lies in determining the new top item. It does Math.Min(num1, VirtualListSize). This basically says that if the current top item is less than the new list size then just keep it otherwise set the top item index to be the new list size. Here is where the exception will occur. If the top item is set to the list size then we'll be off by one (because in a list of size 200 the last indexable value is 199).
As a result if the top item in the list (the first displayed item) is less than the virtual list size then the code will work otherwise it'll crash. The workaround would be to modify your code to set the TopItem before you set the new virtual list size. This would resolve your crash until the issue is fixed. Hopefully MS trowls this forum for bug submissions otherwise you'll need to notify them directly.
Good find,
Michael Taylor - 11/28/05
Excellent analysis, thank you! And yes, I also hope they've got someone scanning this from time to time...
I've seen the same thing. In my case, I just used a try-catch block to circumvent this bug (which is bad, but the only option I had):
| | try { // HACK: Catch exceptions sometimes thrown by ListView bug listView.VirtualListSize = virtualList.Count; } catch (ArgumentOutOfRangeException) { Debug.WriteLine("(VirtualListViewClient caught VirtualListSize exception)"); } catch (NullReferenceException) { Debug.WriteLine("(VirtualListViewClient caught VirtualListSize exception)"); } |
Notice how I also got a NullReferenceException on occasion, which might be a related bug? It appears less frequently than the ArgumentOutOfRangeException, but it also keeps occurring...
That's basically my solution, too. And the listview seeems to be fine afterwards, thanks to GC. Let's hope no resources are getting lost.
I don't believe this is a glitch.
When you have the listview set with a large number of items and scroll down, the currently loaded index is still stored even when you change the virtuallistsize property. As a result, the index is out of bounds with the newly reset dimensions and an error is thrown. To get around this simply add:
| | ListView1.EnsureVisible(0) |
before you change the size. This makes sure that the currently selected index is lower than the size of the list and will not create the out of bounds error.
It is a bug. You can check the code yourself if you like. MS specifically coded for this situation in the bottom portion of the method body. There is no reason why you would need to call
EnsureVisible(0) when the list changes since the control knows that the old item will not exist. That is why they did the whole
Math.Min() thing. Unfortunately it was not coded correctly and evidently missed in testing. Code coverage tools wouldn't have helped here because the executed code is the same in both cases.
However your solution does seem to be more elegant than the SEH suggestion or even setting the
TopItem explicitly. The only thing I don't like about your suggestion is the fact that every time the tree changes the user will be thrown to the top of the tree again. My users really don't like this so I have to go through hoops when changing a tree such that the user doesn't see the tree jump (unless necessary).
Explorer itself is a good example. If you expand a deeply nested directory and then delete the root
Explorer will simply delete the node and leave the tree where it is. This makes it easier and more visually appealing to users. That is why I would write the code using
TopItem. Then the tree changes only if necessary.
Thanks!
Michael Taylor - 12/5/05
Thanks alot guys great stuff. 
Here is a corrected class that provides the intended behavior of System.Windows.Forms.ListView without the bug:
using
System;
using
System.Windows.Forms;using
System.ComponentModel; /// <summary>
/// Creates a ListView that fixes for the <see cref="VirtualListSize"/>VirtualListSize bug./// </summary>public class VirtualListView : ListView
{
/// <summary>
/// Gets or sets the number of System.Windows.Forms.ListViewItem objects contained/// in the item cache when in virtual mode./// </summary>/// <returns>/// The number of System.Windows.Forms.ListViewItem objects contained in the/// VirtualListView.VirtualListSize when in virtual mode./// </returns>/// <exception cref="System.ArgumentException">/// VirtualListView.VirtualListSize is set to a value less than 0./// </exception>/// <exception cref="System.InvalidOperationException">/// System.Windows.Forms.ListView.VirtualMode is set to true, VirtualListView.VirtualListSize/// is greater than 0, and System.Windows.Forms.ListView.RetrieveVirtualItem is not handled./// </exception>/// <remarks>/// Fixes bug defined at http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=150133&SiteID=1/// </remarks>[
DefaultValue(0)][
RefreshProperties(RefreshProperties.Repaint)]public new int VirtualListSize{
get { return base.VirtualListSize; }set{
// Must set top value to at least one less than value due to // off-by-one error in base.VirtualListSizeint topIndex = this.TopItem == null ? 0 : this.TopItem.Index;topIndex =
Math.Min(topIndex, value - 1);if (topIndex > 0)
this.TopItem = this.Items[topIndex];
base.VirtualListSize = value;
}
}
}