Search code examples
c#winformslistviewvirtualvirtuallistview

Why the Items of a Virtual ListView that are not visible don't have index?


I'm working with a ListView in Virtual Mode (.NET 4.6).
I tried to find the index of Items in the virtual ListView: when I enter a letter, the first item with text that start with that letter should be selected.

Here is the FindItemWithText in listView1_KeyDown:

if (char.IsLetterOrDigit(e.KeyChar))
{
    var str = e.KeyChar.ToString();
    if (tempStr != str)
    {
        Index = 0;
        tempStr = str;
    }

    var item = listView1.FindItemWithText(str, false, Index, true);
    if (item != null)
    {
        item.Selected = true;
        item.Focused = true;
        item.EnsureVisible();
        Index = item.Index + 1;
    }
}

Here is my SearchForVirtualItem method:

var item = lvis.OfType<ListViewItem>().FirstOrDefault(
    i => i.Text.ToLower().StartsWith(e.Text.ToLower()) && 
         i.Index >= e.StartIndex);
if (item == null)
{

}
else
{
    e.Index = item.Index;
}

If the result is one of the visible items before I scroll at all the code works and I can select the result item. But if the result is not visible and I didn't scroll anything at all the method return null.

But if I scroll to the end of the list even once I can get the index of the item that before I couldn't.

Example: If I have 200 items in a virtual list (populated from a list of 200 ListViewItem) and only the first 50 are visible, if I press the c letter and items that start with c letter are among the first 50, they will be selected.
But if I press x and the items in the virtual ListView are at the last 50, the method will return null. If I instead scroll the list to the end and then I press x, the items that start with x will be selected.

Why I have to show the item at least once to have an index and not having index = -1?
Is this the normal behavior of a virtual ListView or is there something wrong?

Side question, when does a ListView in normal mode become slow? After 100,000 items, or 1,000,000 items?

Edit1:
Here is my listView1_RetrieveVirtualItem code:

private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
    if (lvis.Count > 0)
    {
        e.Item = lvis[e.ItemIndex];
    }
}

I don't use a cache.
I use BackGroundWorker to get the data from a SQLite database; I create ListViewitems and add them to a List (var lvis = new List<ListViewItem>).

The RunWorkerCompleted method:

private void Pl_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    var obj = e.Result;
    if (obj != null)
    {
        RemoveSelection();

        lvis = (List<ListViewItem>)obj;
        listView1.Items.Clear();
        listView1.VirtualListSize = lvis.Count;
        listView1.Invalidate();

        var No_of_items = listView1.Items.Count + " pin(s)";
        count.Text = No_of_items;
        tabLabel.Text = GetButton().Text + " | " + No_of_items;
    }
}

lvis is the source where the virtual ListView get its data from.


Solution

  • It looks like it's a simple misunderstanding related to the stored ListViewItem Index value: when you create a ListViewItem, you cannot set the Index, so this method to retrieve and return a matching ListViewItem:

    private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
    {
        var item = lvis.OfType<ListViewItem>().FirstOrDefault([...]); 
        e.Index = item.Index;
    }
    

    will fail: item.Index is always -1, since was never set when the ListViewItem was created.
    That's why the ListView will find Items that have already been shown (these have an Index, the virtual list doesn't need to retrieve them calling SearchForVirtualItem(), it will just call FindItem()).

    A simple solution is to use the List.FindIndex() method, instead of finding an Item using FirstOrDefault(). This method returns the index in the List that contains the object that meets the criteria defined by the Predicate<T> argument.
    This is the value of e.Index that the ListView.SearchForVirtualItem handler is expecting.


    How many items a ListView can hold before it becomes difficult to manage or too slow: without any further specifications, this is a question difficult to answer. It may behave perfectly fine with 100000 items in List mode (as in the example), but setting the View = View.Details may change the scenario completely. Does it have to also handle graphics object? Well, how large? How many handles are needed, in this case? In practice, it's a question you answer yourself testing different scenarios.
    The User perspective is also to consider (or should it come first? :). Maybe the list is scrollable with ease, but is it also easy to locate a specific Item?
    If you have a lot of Items to present in the UI, you should most probably organize them in sub cathegories and provide easy, quick, visual methods to search and filter them, so your Users end up working with much less crowded subsets, probably closer to what they actually need to use or find.


    Here's a fix and a code sample that should allow to test the functionality of the ListView.FindItemWithText() method (this one also needs a small tweak).

    • The ListView.VirtualMode is set in the Designer
    • In the example, the ListViewItems collection is represented by a list of 1,000 items, repeated 100 times, so the ListView VirtualListSize is set to 100,000 items

    ListView VirtualMode Search Items

    btnLVSearch: the Button used to search the ListView items.
    btnLVLoadData: the Button used to load the data and sets the VirtualListSize.
    chkPrefixSearch: the CheckBox that selects a PrefixSearch or a TextSearch.
    chkCaseSensitiveSearch: the CheckBox used to set/reset the case sensitive search

    int currentStartIndex = 0;
    List<ListViewItem> listItems = null;
    
    private void btnLVLoadData_Click(object sender, EventArgs e)
    {
        listItems = new List<ListViewItem>();
        // [...]
        //  Fill the listItems collection  
        listView1.VirtualListSize = listItems.Count;
    }
    
    private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
    {
        if (e.ItemIndex >= 0) {
            e.Item = listItems[e.ItemIndex];
        }
    }
    
    private void listView1_SearchForVirtualItem(object sender, SearchForVirtualItemEventArgs e)
    {
        StringComparison comparison = chkCaseSensitiveSearch.Checked 
                                    ? StringComparison.CurrentCulture 
                                    : StringComparison.CurrentCultureIgnoreCase;
        int itemIndex = -1;
        if (e.IsPrefixSearch) {
            itemIndex = listItems.FindIndex(e.StartIndex, 
                itm => itm.Text.StartsWith(e.Text, comparison));
        }
        else if (e.IsTextSearch) {
            itemIndex = listItems.FindIndex(e.StartIndex, 
                itm => itm.Text.IndexOf(e.Text, comparison) >= 0);
        }
        e.Index = itemIndex;
    }
    
    private void btnLVSearch_Click(object sender, EventArgs e)
    {
        var item = listView1.FindItemWithText(
            txtLVSearch.Text, false, currentStartIndex, chkPrefixSearch.Checked);
    
        if (item != null) {
            currentStartIndex = item.Index + 1;
            listView1.SelectedIndices.Add(item.Index);
            item.Selected = true;
            listView1.EnsureVisible(item.Index);
            listView1.Focus();
        }
        else {
            currentStartIndex = 0;
        }
    }
    

    When handling the ListView.KeyPress event, set e.Handled = true to suppress the key press, otherwise a second SearchForVirtualItem event is triggered immediately after e.Index = itemIndex is assigned (this time, with e.IsPrefixSearch set to false):

    private void listView1_KeyPress(object sender, KeyPressEventArgs e)
    {
        e.Handled = true;
        var item = listView1.FindItemWithText(
            e.KeyChar.ToString(), false, currentStartIndex, chkPrefixSearch.Checked);
        // [...]
    }