How to SelectAll in a WinForms virtual ListView?

2019-05-06 13:13发布

问题:

What is the correct/managed way to SelectAll in a .NET listview that is in virtual mode?

When a ListView's VirtualMode is enabled, the notion of selecting a ListViewItem goes away. The only thing you select are indexes. These are accessible through the SelectedIndices property.

Workaround #1

The first hack is to add iteratively add every index to the SelectedIncides collection:

this.BeginUpdate();
try
{
    for (int i = 0; i < this.VirtualListSize; i++)
        this.SelectedIndices.Add(i);
}
finally     
{
    this.EndUpdate();
}

In addition to being poorly designed (a busy loop counting to a hundred thousand), it's poorly performing (it throws an OnSelectedIndexChanged event every iteration). Given that the listview is in virtual mode, it is not unreasonable to expect that there will be quite a few items in the list.

Workaround #2

The Windows ListView control is fully capable of selecting all items at once. Sending the listview a LVM_SETITEMSTATE message, telling it so "select" all items.:

LVITEM lvi;
lvi.stateMask = 2; //only bit 2 (LVIS_SELECTED) is valid
lvi.state = 2;     //setting bit two on (i.e. selected)

SendMessage(listview.Handle, LVM_SETITEMSTATE, -1, lvi); //-1 = apply to all items

This works well enough. It happens instantly, and at most only two events are raised:

class NativeMethods
{
    private const int LVM_SETITEMSTATE = LVM_FIRST + 43;

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public struct LVITEM
    {
        public int mask;
        public int iItem;
        public int iSubItem;
        public int state;
        public int stateMask;
        [MarshalAs(UnmanagedType.LPTStr)]public string pszText;
        public int cchTextMax;
        public int iImage;
        public IntPtr lParam;
        public int iIndent;
        public int iGroupId;
        public int cColumns;
        public IntPtr puColumns;
    };

    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    public static extern IntPtr SendMessageLVItem(HandleRef hWnd, int msg, int wParam, ref LVITEM lvi);

    /// <summary>
    /// Select all rows on the given listview
    /// </summary>
    /// <param name="listView">The listview whose items are to be selected</param>
    public static void SelectAllItems(ListView listView)
    {
        NativeMethods.SetItemState(listView, -1, 2, 2);
    }

    /// <summary>
    /// Set the item state on the given item
    /// </summary>
    /// <param name="list">The listview whose item's state is to be changed</param>
    /// <param name="itemIndex">The index of the item to be changed</param>
    /// <param name="mask">Which bits of the value are to be set?</param>
    /// <param name="value">The value to be set</param>
    public static void SetItemState(ListView listView, int itemIndex, int mask, int value)
    {
        LVITEM lvItem = new LVITEM();
        lvItem.stateMask = mask;
        lvItem.state = value;
        SendMessageLVItem(new HandleRef(listView, listView.Handle), LVM_SETITEMSTATE, itemIndex, ref lvItem);
    }
}

But it relies on P/Invoke interop. It also relies on the truth that the .NET ListView is a wrapper around the Windows ListView control. This is not always true.

So i'm hoping for the correct, managed, way to SelectAll is a .NET WinForms ListView.

Bonus Chatter

There is no need to resort to P/Invoke in order to deselect all items in a listview:

LVITEM lvi;
lvi.stateMask = 2; //only bit 2 (LVIS_SELECTED) is valid
lvi.state = 1;     //setting bit two off (i.e. unselected)

SendMessage(listview.Handle, LVM_SETITEMSTATE, -1, lvi); //-1 = apply to all items

the managed equivalent is just as fast:

listView.SelectedIndices.Clear();

Bonus Reading

  • Ian Boyd wonders how to SelectAll items of a non-virtual ListView
  • Making "select all" faster definitely helps someone who wants to add Ctrl+A support