为什么在修改所选项目时会在 ListBox 中触发 SelectedIndexChanged 事件?
Why does the SelectedIndexChanged event fire in a ListBox when the selected item is modified?
我们得到了一个从 Microsoft Visual Studio 的模板(PasteBin 1 2 3 4 上的设计器代码)创建的 Windows 表单应用程序,带有默认的 ListBox exampleListBox
和 Button exampleButton
.
我们用 1 到 10 的数字填充列表框。
for (int i = 0; i < 10; ++i)
{
exampleListBox.Items.Add(i);
}
然后我们添加两个事件处理程序。
exampleListBox_SelectedIndexChanged
只会将当前选定的索引写入控制台。
private void exampleListBox_SelectedIndexChanged(object sender, EventArgs e)
{
Console.WriteLine(exampleListBox.SelectedIndex);
}
exampleButton_Click
会将当前选定索引处的项目设置为其自身。如此有效,这绝对不会改变任何东西。
private void exampleButton_Click(object sender, EventArgs e)
{
exampleListBox.Items[exampleListBox.SelectedIndex] = exampleListBox.Items[exampleListBox.SelectedIndex];
}
单击按钮时,我希望不会发生任何事情。然而,这种情况并非如此。单击该按钮会触发 exampleListBox_SelectedIndexChanged
事件,即使 SelectedIndex
没有更改。
例如,如果我点击exampleListBox
中索引2处的项目,那么exampleListBox.SelectedIndex
就会变成2。如果我按exampleButton
,那么exampleListBox.SelectedIndex
仍然是2. 然而,然后 exampleListBox_SelectedIndexChanged
事件触发。
为什么即使所选索引未更改,事件仍会触发?
此外,是否有任何方法可以防止这种行为发生?
当您修改 ListBox 中的项目(或者实际上是 ListBox 的关联 ObjectCollection 中的项目)时,底层代码实际上会删除并重新创建该项目。然后它选择这个新添加的项目。因此,所选索引已发生更改,并引发相应的事件。
我没有特别令人信服的解释为什么控件会这样。它要么是为了编程方便而完成的,要么只是 WinForms 原始版本中的一个错误,出于向后兼容的原因,后续版本必须保持该行为。此外,即使项目未被修改,后续版本也必须保持相同的行为。这是您观察到的违反直觉的行为。
而且,遗憾的是,它是 not documented——除非你明白它为什么会发生,然后你知道 SelectedIndex 属性 实际上 是 在你不知情的情况下在幕后发生了变化。
Quantic 发表评论指向 the relevant portion of the code in the Reference Source:
internal void SetItemInternal(int index, object value) {
if (value == null) {
throw new ArgumentNullException("value");
}
if (index < 0 || index >= InnerArray.GetCount(0)) {
throw new ArgumentOutOfRangeException("index", SR.GetString(SR.InvalidArgument, "index", (index).ToString(CultureInfo.CurrentCulture)));
}
owner.UpdateMaxItemWidth(InnerArray.GetItem(index, 0), true);
InnerArray.SetItem(index, value);
// If the native control has been created, and the display text of the new list item object
// is different to the current text in the native list item, recreate the native list item...
if (owner.IsHandleCreated) {
bool selected = (owner.SelectedIndex == index);
if (String.Compare(this.owner.GetItemText(value), this.owner.NativeGetItemText(index), true, CultureInfo.CurrentCulture) != 0) {
owner.NativeRemoveAt(index);
owner.SelectedItems.SetSelected(index, false);
owner.NativeInsert(index, value);
owner.UpdateMaxItemWidth(value, false);
if (selected) {
owner.SelectedIndex = index;
}
}
else {
// NEW - FOR COMPATIBILITY REASONS
// Minimum compatibility fix for VSWhidbey 377287
if (selected) {
owner.OnSelectedIndexChanged(EventArgs.Empty); //will fire selectedvaluechanged
}
}
}
owner.UpdateHorizontalExtent();
}
在这里,您可以看到,在最初的 运行 次错误检查之后,它会更新 ListBox 的最大项目宽度,在内部数组中设置指定的项目,然后检查本机是否ListBox 控件已创建。实际上,所有 WinForms 控件都是本机 Win32 控件的包装器,ListBox 也不例外。在您的示例中,本机控件肯定已创建,因为它在表单上可见,因此 if (owner.IsHandleCreated)
测试评估为真。然后比较项目的文本以查看它们是否相同:
如果它们不同,则删除原始项目,删除选择,添加新项目,如果选择了原始项目,则选择它。这会引发 SelectedIndexChanged 事件。
如果它们相同且当前选中该项目,则如注释所示,"for compatibility reasons",手动引发 SelectedIndexChanged 事件。
我们刚刚分析的这个 SetItemInternal
方法是从 setter 调用的 ListBox.ObjectCollection 对象的默认值 属性:
public virtual object this[int index] {
get {
if (index < 0 || index >= InnerArray.GetCount(0)) {
throw new ArgumentOutOfRangeException("index", SR.GetString(SR.InvalidArgument, "index", (index).ToString(CultureInfo.CurrentCulture)));
}
return InnerArray.GetItem(index, 0);
}
set {
owner.CheckNoDataSource();
SetItemInternal(index, value);
}
}
这是您的代码在 exampleButton_Click
事件处理程序中调用的内容。
无法阻止这种行为的发生。您必须通过在 SelectedIndexChanged 事件处理程序方法中编写您自己的代码来找到解决它的方法。您可能会考虑从内置列表框 class 派生自定义控件 class,覆盖 OnSelectedIndexChanged 方法,并将解决方法放在这里。这个派生的 class 将为您提供一个方便的位置来存储状态跟踪信息(作为成员变量),并且它将允许您在整个项目中使用修改后的 ListBox 控件作为直接替换,而无需修改SelectedIndexChanged 事件处理程序无处不在。
但老实说,这应该不是什么大问题,也不是您需要解决的任何问题。您对 SelectedIndexChanged 事件的处理应该是微不足道的——只需更新表单上的某些状态,例如依赖控件。如果没有发生外部可见的变化,它触发的变化本身基本上是空操作。
科迪·格雷在上一个答案中给出了解决方案。我的代码示例:
private bool lbMeas_InhibitEvent = false; // "some state on your form"
private void lbMeas_SelectedIndexChanged(object sender, EventArgs e)
{
// when inhibit is found, disarm it and return without action
if (lbMeas_InhibitEvent) { lbMeas_InhibitEvent = false; return; }
// ... find the new item
string cNewItem = "ABCD";
// set new item content, make sure Inhibit is armed
lbMeas_InhibitEvent = true;
// now replace the currently selected item
lbMeas.Items[lbMeas.SelectedIndex] = cNewItem;
// ... your code will proceed here
}
我们得到了一个从 Microsoft Visual Studio 的模板(PasteBin 1 2 3 4 上的设计器代码)创建的 Windows 表单应用程序,带有默认的 ListBox exampleListBox
和 Button exampleButton
.
我们用 1 到 10 的数字填充列表框。
for (int i = 0; i < 10; ++i)
{
exampleListBox.Items.Add(i);
}
然后我们添加两个事件处理程序。
exampleListBox_SelectedIndexChanged
只会将当前选定的索引写入控制台。
private void exampleListBox_SelectedIndexChanged(object sender, EventArgs e)
{
Console.WriteLine(exampleListBox.SelectedIndex);
}
exampleButton_Click
会将当前选定索引处的项目设置为其自身。如此有效,这绝对不会改变任何东西。
private void exampleButton_Click(object sender, EventArgs e)
{
exampleListBox.Items[exampleListBox.SelectedIndex] = exampleListBox.Items[exampleListBox.SelectedIndex];
}
单击按钮时,我希望不会发生任何事情。然而,这种情况并非如此。单击该按钮会触发 exampleListBox_SelectedIndexChanged
事件,即使 SelectedIndex
没有更改。
例如,如果我点击exampleListBox
中索引2处的项目,那么exampleListBox.SelectedIndex
就会变成2。如果我按exampleButton
,那么exampleListBox.SelectedIndex
仍然是2. 然而,然后 exampleListBox_SelectedIndexChanged
事件触发。
为什么即使所选索引未更改,事件仍会触发?
此外,是否有任何方法可以防止这种行为发生?
当您修改 ListBox 中的项目(或者实际上是 ListBox 的关联 ObjectCollection 中的项目)时,底层代码实际上会删除并重新创建该项目。然后它选择这个新添加的项目。因此,所选索引已发生更改,并引发相应的事件。
我没有特别令人信服的解释为什么控件会这样。它要么是为了编程方便而完成的,要么只是 WinForms 原始版本中的一个错误,出于向后兼容的原因,后续版本必须保持该行为。此外,即使项目未被修改,后续版本也必须保持相同的行为。这是您观察到的违反直觉的行为。
而且,遗憾的是,它是 not documented——除非你明白它为什么会发生,然后你知道 SelectedIndex 属性 实际上 是 在你不知情的情况下在幕后发生了变化。
Quantic 发表评论指向 the relevant portion of the code in the Reference Source:
internal void SetItemInternal(int index, object value) {
if (value == null) {
throw new ArgumentNullException("value");
}
if (index < 0 || index >= InnerArray.GetCount(0)) {
throw new ArgumentOutOfRangeException("index", SR.GetString(SR.InvalidArgument, "index", (index).ToString(CultureInfo.CurrentCulture)));
}
owner.UpdateMaxItemWidth(InnerArray.GetItem(index, 0), true);
InnerArray.SetItem(index, value);
// If the native control has been created, and the display text of the new list item object
// is different to the current text in the native list item, recreate the native list item...
if (owner.IsHandleCreated) {
bool selected = (owner.SelectedIndex == index);
if (String.Compare(this.owner.GetItemText(value), this.owner.NativeGetItemText(index), true, CultureInfo.CurrentCulture) != 0) {
owner.NativeRemoveAt(index);
owner.SelectedItems.SetSelected(index, false);
owner.NativeInsert(index, value);
owner.UpdateMaxItemWidth(value, false);
if (selected) {
owner.SelectedIndex = index;
}
}
else {
// NEW - FOR COMPATIBILITY REASONS
// Minimum compatibility fix for VSWhidbey 377287
if (selected) {
owner.OnSelectedIndexChanged(EventArgs.Empty); //will fire selectedvaluechanged
}
}
}
owner.UpdateHorizontalExtent();
}
在这里,您可以看到,在最初的 运行 次错误检查之后,它会更新 ListBox 的最大项目宽度,在内部数组中设置指定的项目,然后检查本机是否ListBox 控件已创建。实际上,所有 WinForms 控件都是本机 Win32 控件的包装器,ListBox 也不例外。在您的示例中,本机控件肯定已创建,因为它在表单上可见,因此 if (owner.IsHandleCreated)
测试评估为真。然后比较项目的文本以查看它们是否相同:
如果它们不同,则删除原始项目,删除选择,添加新项目,如果选择了原始项目,则选择它。这会引发 SelectedIndexChanged 事件。
如果它们相同且当前选中该项目,则如注释所示,"for compatibility reasons",手动引发 SelectedIndexChanged 事件。
我们刚刚分析的这个 SetItemInternal
方法是从 setter 调用的 ListBox.ObjectCollection 对象的默认值 属性:
public virtual object this[int index] {
get {
if (index < 0 || index >= InnerArray.GetCount(0)) {
throw new ArgumentOutOfRangeException("index", SR.GetString(SR.InvalidArgument, "index", (index).ToString(CultureInfo.CurrentCulture)));
}
return InnerArray.GetItem(index, 0);
}
set {
owner.CheckNoDataSource();
SetItemInternal(index, value);
}
}
这是您的代码在 exampleButton_Click
事件处理程序中调用的内容。
无法阻止这种行为的发生。您必须通过在 SelectedIndexChanged 事件处理程序方法中编写您自己的代码来找到解决它的方法。您可能会考虑从内置列表框 class 派生自定义控件 class,覆盖 OnSelectedIndexChanged 方法,并将解决方法放在这里。这个派生的 class 将为您提供一个方便的位置来存储状态跟踪信息(作为成员变量),并且它将允许您在整个项目中使用修改后的 ListBox 控件作为直接替换,而无需修改SelectedIndexChanged 事件处理程序无处不在。
但老实说,这应该不是什么大问题,也不是您需要解决的任何问题。您对 SelectedIndexChanged 事件的处理应该是微不足道的——只需更新表单上的某些状态,例如依赖控件。如果没有发生外部可见的变化,它触发的变化本身基本上是空操作。
科迪·格雷在上一个答案中给出了解决方案。我的代码示例:
private bool lbMeas_InhibitEvent = false; // "some state on your form"
private void lbMeas_SelectedIndexChanged(object sender, EventArgs e)
{
// when inhibit is found, disarm it and return without action
if (lbMeas_InhibitEvent) { lbMeas_InhibitEvent = false; return; }
// ... find the new item
string cNewItem = "ABCD";
// set new item content, make sure Inhibit is armed
lbMeas_InhibitEvent = true;
// now replace the currently selected item
lbMeas.Items[lbMeas.SelectedIndex] = cNewItem;
// ... your code will proceed here
}