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:
- Select the third item (with index 2)
- Check "freeze selection"
- Click on the forth item
- Uncheck "freeze selection"
- 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):
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.
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;
There are now no issues going back to a state where freeze selection
is unchecked and selecting some new item.
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;
}
}