5

I need to keep track of the selected item on a ListBox to update/disable other controls according to the currently selected value.

This is the code to reproduce the issue:

public partial class Form1 : Form
{
    private readonly BindingList<string> List = new BindingList<string>();

    public Form1()
    {
        InitializeComponent();
        listBox1.DataSource = List;

        listBox1.SelectedValueChanged += (s, e) => System.Diagnostics.Debug.WriteLine("VALUE");
        listBox1.SelectedIndexChanged += (s, e) => System.Diagnostics.Debug.WriteLine("INDEX");

        addButton.Click += (s, e) => List.Add("Item " + (List.Count + 1));
        removeButton.Click += (s, e) => List.RemoveAt(List.Count - 1);

        logSelectionButton.Click += (s, e) =>
        {
            System.Diagnostics.Debug.WriteLine("Selected Index: " + listBox1.SelectedIndex);
            System.Diagnostics.Debug.WriteLine("Selected Value: " + listBox1.SelectedValue);
        };
    }
}

My form has a list box listBox1 and three buttons: addButton, removeButton and logSelectionButton.

If you press addButton (starting with an empty list), then removeButton and finally addButton again, neither SelectedValueChanged nor SelectedIndexChanged will fire at the last addButton press, even though if you press logSelectionButton before and after the last addButton press, you'll see that the values of both SelectedIndex and SelectedValue have changed from -1 to 0 and from null to "Item 1" respectively, and that "Item 1" looks selected on the list box.

This would cause any other controls I need to update according to the selected item to stay disabled until the user manually selects an item on the list box, even though the first item is already selected.

I can't think of any workaround. Perhaps also subscribing to my BindingList's ListChanged event to see whether the list is empty or not, but then I don't know if the items in the list box will be updated before or after my event handler fires, which will cause other problems.

Juan
  • 15,274
  • 23
  • 105
  • 187

2 Answers2

4

Seems like you found a bug in ListControl internal handling of the PositionChanged event when data bound (if you turn Exceptions on in VS, you'll see an exception when the first item is added to the empty list).

Since ListControl derived classes like ListBox, ComboBox etc. in data bound mode synchronize their selection with the Position property of the BindingManagerBase, the reliable workaround (and basically a more general abstract solution) is to handle CurrentChanged event of the underlying data source binding manager:

listBox1.BindingContext[List].CurrentChanged += (s, e) =>
    System.Diagnostics.Debug.WriteLine("CURRENT");
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • This doesn't seem to work. Apparently `CurrentChanged` fires before the selection actually changes, and what I need is to keep track of the currently selected item. I found another solution (see my own response). – Juan Feb 22 '17 at 19:14
  • As you wish. I don't see why you care if control selection is changed when the same is available through `Current` property of the binding manager, and is correct. All automatic master-detail bindings and navigators use that approach rather than reacting to specific control events/properties. – Ivan Stoev Feb 22 '17 at 19:19
  • Didn't know you can get the selected value from `Current`. Nice approach. – Juan Feb 22 '17 at 19:24
0

I found a workaround that seems to work fine. Since ListBox updates the selected index by setting the SelectedIndex property and the property is virtual I can override it to keep track of it:

public class ListBoxThatWorks : ListBox
{
    private int LatestIndex = -1;
    private object LatestValue = null;

    public EqualityComparer<object> ValueComparer { get; set; }

    public override int SelectedIndex
    {
        get { return base.SelectedIndex; }
        set { SetSelectedIndex(value); }
    }

    private void NotifyIndexChanged()
    {
        if (base.SelectedIndex != LatestIndex)
        {
            LatestIndex = base.SelectedIndex;
            base.OnSelectedIndexChanged(EventArgs.Empty);
        }
    }

    private void NotifyValueChanged()
    {
        if (!(ValueComparer ?? EqualityComparer<object>.Default).Equals(LatestValue, base.SelectedValue))
        {
            LatestValue = base.SelectedValue;
            base.OnSelectedValueChanged(EventArgs.Empty);
        }
    }

    private void SetSelectedIndex(int value)
    {
        base.SelectedIndex = value;
        NotifyIndexChanged();
        NotifyValueChanged();
    }
}
Juan
  • 15,274
  • 23
  • 105
  • 187