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!




Answer this question

Virtual ListView crashes on VirtualListSize

  • mcgyver74

    Thanks alot guys great stuff.



  • SamGB

    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

  • wojtek.n

    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. 


  • tgm_0402

    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.

  • Breezy-WA

    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&amp;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.VirtualListSize

    int 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;

    }

    }

    }


  • ges

    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

  • G.Lakshmi

    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...


  • Amy Hagstrom

    Excellent analysis, thank you! And yes, I also hope they've got someone scanning this from time to time...

  • Bob-12

    The code posted by Justin C also has a bug. Using his code, if you populate a virtual ListView with, say 200 items, (enough for the list to be scrollable). Then, scroll all the way down to the bottom of the list and make a call to set the VirtualListSize = 1, you end up getting a similar error as the original problem. This is because if you try to set the value to 1, the line:

    topIndex = Math.Min(topIndex, value - 1);


    will evaluate to 0 (zero). Which is fine (and correct), but unfortunately the next line checks to see if the 'topIndex' variable is greater than 0 before setting the TopItem property. Since it is not greater than zero, the TopItem property remains unchanged and is probably equal to 150 or something (depending on the size of your form). At this point, you end up getting the same exception when you try to set the VirtualListSize property to 1.


    So what you could do is change:


    topIndex = Math.Min(topIndex, value - 1);


    to


    // Compute the top index, making sure not to go negative
    // if a value of 0 (zero) is passed in.

    topIndex = Math.Min(topIndex, Math.Abs(value - 1));



    Then get rid of the if statement:

    if (topIndex > 0)



    and just always set the TopItem property.

    This should handle all cases without any errors.

  • Virtual ListView crashes on VirtualListSize