1
votes

The context is as below:

  • A ComboBox is binded to a BindingSource.

  • That BindingSource is then binded to a DataModel.

  • That ComboBox has a DataSource of a list of objects. Yet, this list maybe set to null when under some conditions, and maybe restored under some other conditions.

My problem is on the last step of the below scenario:

  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

I would like to understand why will it be so, as i don't want the prvious value being preserved after the dataSource has been null.

Here is how I setup the code, with 1 comboBox binded to the a bindingSource and data-model, and 2 buttons to simulate change of comboBox's dataSource.

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

However, if i create a new list, with new items everytime, then the issue will not be seen. However, it is not possible in my actual code as the list is always the same instance, just a matter of put it on datasource or not.

Sample as below:

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

A workaround i did is to check the current dataSource:

If the current dataSource is null, and the new dataSource is not, Then set SelectedIndex = 0

But i would really want to avoid this manual intervention other than changing the datasource.

Appreciate anyone have a solution on that, thanks in adv!

Updated the working code with suggestions from 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";
    }
2

2 Answers

0
votes

You can simply handle the issue by verifying item.count , so if is equal to zero do nothing

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

keep above code where ever you would like

Let me know if it helps

0
votes

The described behavior is caused by the WinForms data binding infrastructure, in particular the BindingContext class. For each unique data source object it maintains a corresponding BindingManageBase object, which along with other things provides Position and Current property for the data sources - something that they normally don't have - consider IList for example. For list type data source it's an instance of a CurrencyManager class and is created the first time binding manager is requested for a data source object. The important part related to your issue is that there is no mechanism of removing the already created binding managers. Think of it like a dictionary with data source as a key, binding manager as a value created the first time you request a non existing key.

ComboBox control is one of the controls that uses and modifies the Position property when you data bind the list portion of it. When you assign an list to the DataSource property, it synchronizes the selection with the Position, which for newly created binding manger for non empty list is 0. Then when you select an item in the combo box, the Position is updated accordingly. When you set DataSource to null, the combo box items are cleared, but the existing binding manager with the last set position remains inside the BindingContext. The next time you set the same list as data source, it gets the existing binding manager with the last set position, hence the behavior you are getting.

To get rid of this behavior, you should use intermediate BindingSource component. Along with other things, it provides it's own Position, Current property/management and dedicated associated CurrencyManager which allows it do be used a regular data source for controls, at the same time hiding the actual data source from them (and in particular from BindingContext).

Here is how I see the solution. Add another BindingSource component (similar to yours cbxBindingSource), let call it for instance listBindingSource and statically bind the combo box to it:

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

Now all you need is to manipulate the DataSource property of the binding source instead of the bound control(s).

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