为什么在修改所选项目时,ListBox中会触发SelectedIndexChanged事件



我们得到了一个Windows窗体应用程序,该应用程序是根据Microsoft Visual Studio的模板(PasteBin 1 2 3 4上的设计器代码)创建的,具有默认的ListBoxexampleListBox和ButtonexampleButton

我们用从1到10的数字填充ListBox。

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];
}

当点击按钮时,我预计什么都不会发生。然而,事实并非如此。即使SelectedIndex没有更改,单击该按钮也会触发exampleListBox_SelectedIndexChanged事件。

例如,如果我单击exampleListBox中索引2处的项目,则exampleListBox.SelectedIndex将变为2。如果我按下exampleButton,那么exampleListBox.SelectedIndex仍然是2。然而,随后exampleListBox_SelectedIndexChanged事件启动。

为什么在所选索引未更改的情况下仍会触发事件

此外,是否有任何方法可以防止这种行为的发生?

当您修改ListBox中的项(或者实际上是ListBox的关联ObjectCollection中的项)时,底层代码实际上会删除并重新创建该项。然后它选择这个新添加的项目。因此,所选索引更改,并引发相应的事件。

我没有特别有说服力的解释为什么控件会这样。它要么是为了编程方便,要么只是WinForms原始版本中的一个错误,由于向后兼容性的原因,后续版本不得不保持这种行为。此外,后续版本必须保持相同的行为,即使项目没有被修改。这是你观察到的反直觉行为。

遗憾的是,它没有被记录下来——除非你理解为什么会发生这种情况,然后你知道SelectedIndex属性实际上正在幕后被更改,而你并不知道。

Quantic在参考来源:中留下了一条指向代码相关部分的评论

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)测试的计算结果为true。然后,它比较项目的文本,看看它们是否相同:

  • 如果它们不同,它会删除原始项目,删除所选内容,添加新项目,并在选择原始项目时选择它。这会引发SelectedIndexChanged事件。

  • 如果它们是相同的,并且项目当前已被选中,则正如注释所示,"出于兼容性原因",将手动引发SelectedIndexChanged事件。

我们刚刚分析的这个SetItemInternal方法是从ListBox.ObjectCollection对象的默认属性的setter调用的

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);
}
}

这是CCD_ 14事件处理程序中代码调用的内容。

没有办法阻止这种行为的发生。您必须通过在SelectedIndexChanged事件处理程序方法中编写自己的代码来找到解决此问题的方法。您可以考虑从内置的ListBox类派生一个自定义控件类,重写OnSelectedIndexChanged方法,并在此处提供解决方法。这个派生类将为您提供一个方便的位置来存储状态跟踪信息(作为成员变量),并且它将允许您在整个项目中使用修改后的ListBox控件作为替换项,而不必到处修改SelectedIndexChanged事件处理程序。

但老实说,这不应该是一个大问题,也不应该是任何你需要解决的问题。对SelectedIndexChanged事件的处理应该很简单——只需更新表单上的一些状态,比如依赖控件。如果没有发生外部可见的更改,那么它触发的更改基本上不会是操作本身。

Cody Gray在最后一个答案中给出了解决方案。我的代码示例:

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
}

最新更新