Home > Software engineering >  Winforms ListView item selection bug
Winforms ListView item selection bug

Time:09-13

I'm making an application that uses a ListView control with MultiSelect = false. In some situations I need to prevent the user from changing the selected item. I thought it would be a simple task, but hours later I'm still trying to figure out what's going on.

So in order to have the option to "freeze" the ListView selection, I made a custom class CListView that inherits from ListView. If FreezeSelection is set to true, every time the users changes the selection, I'm trying to change it back:

public class CListView : ListView
{
    public bool FreezeSelection { get; set; } = false;

    bool _applyingSelectionUpdates = false;

    protected override void OnSelectedIndexChanged(EventArgs e)
    {
        if (FreezeSelection)
        {
            if (_applyingSelectionUpdates)
                return;

            // for simplicity consider that the selected index while the selection is frozen is always 2
            int selectedIndex = 2;

            _applyingSelectionUpdates = true;
            try
            {
                SelectedIndices.Clear();
                if (selectedIndex >= 0)
                    SelectedIndices.Add(selectedIndex);
            }
            finally { _applyingSelectionUpdates = false; }

            return;
        }

        base.OnSelectedIndexChanged(e);
    }
}

The problem is when I set FreezeSelection back to false, and the user tries to select a different item. First of all, even if MultiSelect is false, visually it appears as there are two items selected. But programatically, when the user changes the selection, it seems there is sometimes the correct item selected, sometimes no item selected.

This behaviour is clearly a bug and I suspect what is causing this bug. When the user clicks on an item, the event SelectedIndexChanged is fired twice. Once after the SelectedIndices collection is cleared and the second time after the clicked item is added to the collection of selected items. I think the bug is caused by changing the selected items between these two events, but I need to know more about this. If MultiSelect is true and the user tries to select items with Ctrl, I have no problems.

To reproduce this bug you can use the following TestForm:

public class TestForm : Form
{
    CListView listView;
    CheckBox checkBox;

    public TestForm()
    {
        listView = new() { Dock = DockStyle.Fill, View = View.Details, FullRowSelect = true, MultiSelect = false };
        listView.Columns.Add("col 1");
        listView.SelectedIndexChanged  = ListView_SelectedIndexChanged;
        Controls.Add(listView);

        checkBox = new() { Dock = DockStyle.Right, Text = "freeze selection" };
        checkBox.CheckedChanged  = CheckBox_CheckedChanged;
        Controls.Add(checkBox);

        listView.Items.Add("item 1");
        listView.Items.Add("item 2");
        listView.Items.Add("item 3");
        listView.Items.Add("item 4");
    }

    private void CheckBox_CheckedChanged(object? sender, EventArgs e)
    {
        listView.FreezeSelection = checkBox.Checked;
    }

    DateTime lastSelChangeTime = DateTime.MinValue;
    private void ListView_SelectedIndexChanged(object? sender, EventArgs e)
    {
        if ((DateTime.Now - lastSelChangeTime).TotalMilliseconds > 200)
            Debug.WriteLine(""); // this is just to group together what happens on a single user interaction

        var indices = listView.SelectedIndices.Cast<int>().ToArray();
        Debug.WriteLine("CListView fired selection changed event! "
              DateTime.Now.ToString("h:m:s:fff")   " "
              "{ "   string.Join(", ", indices)   " }");

        lastSelChangeTime = DateTime.Now;
    }
}

If you run this form:

  1. Select the third item (with index 2)
  2. Check "freeze selection"
  3. Click on the forth item
  4. Uncheck "freeze selection"
  5. Try changing the selected item now and observe the bug

The question is how to solve this bug or how to achieve my initial goal (prevent users from selecting a different item).

Update: To clarify, what I refered to as "a bug" is not the fact that I get two events for one selection change (I'm fine with that), it's the inconsistent behaviour between the UI and ListView.SelectedIndices after I "unfreeze" the selected index. I will demonstrate the problem with the following picture (note that each screenshot is taken after I clicked where the cursor is positioned; also the output window shows the SelectedIndices every time I get an SelectedIndexChanged event):

"Bug" demonstration

I use .NET 6.0.

CodePudding user response:

As others have mentioned, there is no bug here as shown in this sequence of selecting Item 1, then Selecting Item2 (which first changes the selection by deselecting Item 1.

screenshot1

If you don't want the User to be selecting things during some arbitrary task (like waiting for a modified document to be saved), why not just set ListView.Enabled to false while you perform the work? In the testcode referenced below, I made an all-in-one for when the checkbox changes that sets the SelectionIndices collection to '2' as in your post;

screenshot2

There are now no issues going back to a state where freeze selection is unchecked and selecting some new item.

screenshot3

public TestForm()
{
    InitializeComponent();
    listView.MultiSelect = false;
    listView.Columns.Add("col 1");
    for (int i = 1; i <= 4; i  ) listView.Items.Add($"Item {i}");

    listView.SelectedIndexChanged  = (sender, e) =>
    {
        richTextBox.AppendLine(
            $"{DateTime.Now} : [{string.Join(", ", listView.SelectedIndices.Cast<int>())}]" );
        var sel =
            listView
            .SelectedItems
            .Cast<ListViewItem>();
        if (sel.Any())
        {
            foreach (var item in sel)
            {
                richTextBox.AppendLine(item);
                richTextBox.AppendLine();
            }
        }
        else richTextBox.AppendLine("No selections", Color.Salmon);
    };

    checkBox.CheckedChanged  = (sender, e) =>
    {
        listView.Enabled = !checkBox.Checked;
        if (checkBox.Checked) doWork();
    };

    void doWork()
    {
        listView.SelectedIndices.Clear();
        listView.SelectedIndices.Add(2);
    }
}

Uses this extension for RichTextBox

static class Extensions
{
    public static void AppendLine(this RichTextBox richTextBox) =>
        richTextBox.AppendText($"{Environment.NewLine}");
    public static void AppendLine(this RichTextBox richTextBox, object text) =>
        richTextBox.AppendText($"{text}{Environment.NewLine}");

    public static void AppendLine(this RichTextBox richTextBox, object text, Color color)
    {
        var colorB4 = richTextBox.SelectionColor;
        richTextBox.SelectionColor = color;
        richTextBox.AppendText($"{text}{Environment.NewLine}");
        richTextBox.SelectionColor = colorB4;
    }
}
  • Related