Winform的combox的selected item在datasource设置为null时缓存,然后返回到原始列表

Winform's combox's selected item is cached when the datasource is set to null, then back to the original list

上下文如下:

我的问题出在以下场景的最后一步:

  1. At the begining, ComboBox's dataSource is a List of 3 items (eg. item1, item2, item3)
  2. Now comboBox show item1 as the default selected item
  3. User pick item 3 on comboBox
  4. Now comboBox show item3 as the selected item
  5. ...some other event happened and cause comboBox's dataSource become null
  6. Now comboBox has no selected item (which selectedIndex is -1)
  7. ...some other event happened and cause comboBox's dataSource become the original list

    8. Now comboBox's selected item is back to item3, but NOT item1

我想明白为什么会这样,因为我不希望数据源为空后保留先前的值。

这是我设置代码的方式,1 个组合框绑定到绑定源和数据模型,2 个按钮模拟组合框数据源的变化。

    BindingModel model = new BindingModel();

    public Form1()
    {
        InitializeComponent();

        // BindingSource <--> model
        cbxBindingSource.DataSource = typeof(BindingModel);
        cbxBindingSource.Add(model);

        // ComboxBox <-SelectedValue-> BindingSource <--> model.Result
        comboBox.DataBindings.Add(new Binding("SelectedValue", cbxBindingSource, "Result", true, DataSourceUpdateMode.OnPropertyChanged));
    }

    // Simulate a re-usable list
    List<BinderHelper<string>> list = new List<BinderHelper<string>>
        {
            new BinderHelper<string>("Item1", "1"),
            new BinderHelper<string>("Item2", "2"),
            new BinderHelper<string>("Item3", "3"),
            new BinderHelper<string>("Item4", "4"),
        };

    private void button2_Click(object sender, EventArgs e)
    {
        // Simulate triggers that will result in a well-populated data-source
        comboBox.DisplayMember = "DisplayMember";
        comboBox.ValueMember = "ValueMember";

        comboBox.DataSource = list;
    }

    private void button3_Click(object sender, EventArgs e)
    {
        // Simulate triggers that will result in a empty data-source
        comboBox.DataSource = null;
    }

    private class BindingModel
    {
        // Simple model to bind to the selected value of the comboBox
        public string Result { get; set; }
    }

    private class BinderHelper<T>
    {
        // Use for binding purpose
        public string DisplayMember { get; set; }
        public T ValueMember { get;set; }

        public BinderHelper(string display, T value)
        {
            DisplayMember = display;
            ValueMember = value;
        }
    }

但是,如果我创建一个新列表,每次都有新项目,那么问题就不会出现。 但是,在我的实际代码中这是不可能的,因为列表始终是相同的实例,只是将其放在数据源上的问题。

示例如下:

    private void button2_Click(object sender, EventArgs e)
    {
        // Create new list everytime
        List<BinderHelper<string>> list = new List<BinderHelper<string>>
        {
            new BinderHelper<string>("Item1", "1"),
            new BinderHelper<string>("Item2", "2"),
            new BinderHelper<string>("Item3", "3"),
            new BinderHelper<string>("Item4", "4"),
        };

        // Simulate triggers that will result in a well-populated data-source
        comboBox.DisplayMember = "DisplayMember";
        comboBox.ValueMember = "ValueMember";

        comboBox.DataSource = list;
    }

我所做的解决方法是检查当前数据源:

如果当前数据源为空,而新数据源不是, 然后设置 SelectedIndex = 0

但除了更改数据源之外,我真的很想避免这种手动干预。

感谢任何对此有解决方案的人,谢谢!

根据 Ivan 的建议更新了工作代码:

    public Form1()
    {
        InitializeComponent();

        model = new BindingModel();

        // BindingSource <--> model
        cbxToModelBindingSource.DataSource = typeof (BindingModel);
        cbxToModelBindingSource.Add(model);

        // New dedicated source to provide the list in combox
        // BindingSource <--> comboBox
        listToCbxBindingSource = new BindingSource(components);
        listToCbxBindingSource.DataSource = typeof (BinderHelper<string>);
        comboBox.DataSource = listToCbxBindingSource;

        // ComboxBox <--SelectedValue--> BindingSource <--> model.Result
        comboBox.DataBindings.Add(new Binding("SelectedValue", cbxToModelBindingSource, "Result", true, DataSourceUpdateMode.OnPropertyChanged));
    }

    // Simulate triggers that will result in a well-populated data-source
    private void button2_Click(object sender, EventArgs e)
    {
        listToCbxBindingSource.DataSource = list;
    }

    // Simulate triggers that will result in a empty data-source
    private void button3_Click(object sender, EventArgs e)
    {
        listToCbxBindingSource.DataSource = null;
    }

    // Additional button to test two-way-bindings
    private void button4_Click(object sender, EventArgs e)
    {
        model.Result = "4";
    }

你可以简单地通过验证 item.count 来处理这个问题,所以如果等于零什么都不做

if (Combo1.item.count==0)
{
    return;
}

将上面的代码保留在任何你想要的地方

如果有帮助请告诉我

所描述的行为是由 WinForms 数据绑定基础结构引起的,特别是数据源的 BindingContext class. For each unique data source object it maintains a corresponding BindingManageBase object, which along with other things provides Position and Current 属性 - 它们通常没有的东西 - 考虑 IList例如。对于列表类型数据源,它是 CurrencyManager class 的一个实例,并且是在第一次为数据源对象请求绑定管理器时创建的。与您的问题相关的重要部分是没有删除已创建的绑定管理器的机制。可以把它想象成一个以数据源为键的字典,绑定管理器作为你第一次请求一个不存在的键时创建的值。

ComboBox 控件是在数据绑定它的列表部分时使用和修改 Position 属性 的控件之一。当您将列表分配给 DataSource 属性 时,它会将 selection 与 Position 同步,对于新创建的非空列表绑定管理器来说,它是 0.然后,当您 select 组合框中的项目时, Position 会相应更新。当您将 DataSource 设置为 null 时,组合框项目将被清除,但具有最后设置位置的现有绑定管理器仍保留在 BindingContext 内。下次您将相同的列表设置为数据源时,它会获取具有最后设置位置的现有绑定管理器,因此会出现您所获得的行为。

要摆脱这种行为,您应该使用中间 BindingSource 组件。连同其他东西,它提供了它自己的 PositionCurrent property/management 和专用关联 CurrencyManager,这允许它同时用作控件的常规数据源向他们隐藏实际数据源(特别是 BindingContext)。

以下是我对解决方案的看法。添加另一个 BindingSource 组件(类似于您的 cbxBindingSource),例如调用它 listBindingSource 并将组合框静态绑定到它:

public Form1()
{
    InitializeComponent();

    // Make properties of the expected object type available for binding
    listBindingSource.DataSource = typeof(BinderHelper<string>);

    comboBox.DisplayMember = "DisplayMember";
    comboBox.ValueMember = "ValueMember";
    comboBox.DataSource = listBindingSource;

    // the rest ...
}

现在您只需操纵绑定源的 DataSource 属性 而不是绑定控件。

private void button2_Click(object sender, EventArgs e)
{
    // Simulate triggers that will result in a well-populated data-source
    listBindingSource.DataSource = list;
}

private void button3_Click(object sender, EventArgs e)
{
    // Simulate triggers that will result in a empty data-source
    listBindingSource.DataSource = null;
}