为什么当更改ListBox中所选项时,SelectedIndexChanged事件会被触发?

10
我们得到了一个由Microsoft Visual Studio模板创建的Windows窗体应用程序(设计师代码在PasteBin 1 2 3 4中),其中包含一个默认的ListBox exampleListBox和一个Button exampleButton
我们将ListBox中的数字填充为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);
}
< p > 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事件随后就会触发。 为什么即使未更改所选索引,事件仍会触发? 此外,是否有任何方法可以防止发生这种行为?

4
我可能错了,但我认为给列表框赋值会运行internal void SetItemInternal(int index, object value)函数,并且如果该项与已有项相同,则运行以下代码(注意注释):// NEW - FOR COMPATIBILITY REASONS // Minimum compatibility fix for VSWhidbey 377287 if (selected) { owner.OnSelectedIndexChanged(EventArgs.Empty); //will fire selectedvaluechanged - Quantic
1
@CloseVoter 为什么这个帖子不适合发布?它包含了所需的行为,而且不是排版错误、作业帮助或建议。 - Zsw
为什么你期望什么都不会发生...你正在分配 SelectedIndex,你应该检查是否 SelectedIndex >= 0,然后你是否使用调试器来检查 SelectedIndex 的值? - MethodMan
@MethodMan 我并没有直接给 SelectedIndex 赋值。是的,我确实检查了 SelectedIndex 的值。例如,如果我选择索引为2的项目,则 SelectedIndex 将变为2。然后我按下按钮,SelectedIndex 仍然是2。 - Zsw
你可以存储 lastSelectedIndex 的值,每次 SelectedIndexChanged 被触发时,将上一次存储的值与当前值进行比较,以确定它是否实际上已更改。 说实话,虽然这是一个“bug”,但我更喜欢保留这种行为。例如,当项目更改时,我希望收到通知,无论所选索引是否更改。 - Thariq Nugrohotomo
2个回答

14

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

我没有特别令人信服的解释,解释为什么控件会以这种方式运行。这可能是为了编程方便,或者仅仅是在最初的WinForms版本中出现了bug,而后续版本必须为了向后兼容性而保持这种行为。此外,即使未修改项目,后续版本也必须保持相同的行为。这就是您观察到的违反直觉的行为

不幸的是,它没有被记录,除非你了解其中的原因,你才知道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);
    }
}

这是您在exampleButton_Click事件处理程序中调用的方法。

无法阻止此行为发生。您需要编写自己的代码来解决SelectedIndexChanged事件处理程序方法中的问题。您可以考虑从内置ListBox类派生自定义控件类,覆盖OnSelectedIndexChanged方法,并将您的解决方案放在此处。这个派生类将为您提供一个方便的地方来存储状态跟踪信息(作为成员变量),并允许您在整个项目中使用修改后的ListBox控件作为一种方便的替代品,而不必在所有地方修改SelectedIndexChanged事件处理程序。

但实际上,这不应该是一个大问题,也不需要您去解决。您对SelectedIndexChanged事件的处理应该很简单——只需更新表单上的某些状态,如相关控件。如果没有外部可见的更改发生,它触发的更改本身基本上是无操作。


0
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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接