views:

405

answers:

3

This really seems like a bug to me, but perhaps some databinding gurus can enlighten me? (My WinForms databinding knowledge is quite limited.)

I have a ComboBox bound to a sorted DataView. When the properties of the items in the DataView change such that items are resorted, the SelectedItem in my ComboBox does not keep in-sync. It seems to point to someplace completely random. Is this a bug, or am I missing something in my databinding?

Here is a sample application that reproduces the problem. All you need is a Button and a ComboBox:

public partial class Form1 : Form
{
    private DataTable myData;

    public Form1()
    {
        this.InitializeComponent();

        this.myData = new DataTable();
        this.myData.Columns.Add("ID", typeof(int));
        this.myData.Columns.Add("Name", typeof(string));
        this.myData.Columns.Add("LastModified", typeof(DateTime));
        this.myData.Rows.Add(1, "first", DateTime.Now.AddMinutes(-2));
        this.myData.Rows.Add(2, "second", DateTime.Now.AddMinutes(-1));
        this.myData.Rows.Add(3, "third", DateTime.Now);

        this.myData.DefaultView.Sort = "LastModified DESC";
        this.comboBox1.DataSource = this.myData.DefaultView;
        this.comboBox1.ValueMember = "ID"; 
        this.comboBox1.DisplayMember = "Name";
    }

    private void saveStuffButton_Click(object sender, EventArgs e)
    {
        DataRowView preUpdateSelectedItem = (DataRowView)this.comboBox1.SelectedItem;
        // OUTPUT: SelectedIndex = 0; SelectedItem.Name = third
        Debug.WriteLine(string.Format("SelectedIndex = {0:N0}; SelectedItem.Name = {1}", this.comboBox1.SelectedIndex, preUpdateSelectedItem["Name"]));

        this.myData.Rows[0]["LastModified"] = DateTime.Now;

        DataRowView postUpdateSelectedItem = (DataRowView)this.comboBox1.SelectedItem;
        // OUTPUT: SelectedIndex = 2; SelectedItem.Name = second
        Debug.WriteLine(string.Format("SelectedIndex = {0:N0}; SelectedItem.Name = {1}", this.comboBox1.SelectedIndex, postUpdateSelectedItem["Name"]));

        // FAIL!
        Debug.Assert(object.ReferenceEquals(preUpdateSelectedItem, postUpdateSelectedItem));
    }
}

To clarify:

  • I understand how I would fix the simple application above--I only included that to demonstrate the problem. My concern is how to fix it when the updates to the underlying data rows could be happening anywhere (on another form, perhaps.)
  • I would really like to still receive updates, inserts, deletes, etc. to my data source. I have tried just binding to an array of DataRows severed from the DataTable, but this causes additional headaches.
A: 

Your example sorts the data on the column it updates. When the update occurs, the order of the rows changes. The combobox is using the index to keep track of it's selected items, so when the items are sorted, the index is pointing to a different row. You'll need to capture the value of comboxBox1.SelectedItem before updating the row, and set it back once the update is complete:

        DataRowView selected = (DataRowView)this.comboBox1.SelectedItem;
        this.myData.Rows[0]["LastModified"] = DateTime.Now;
        this.comboBox1.SelectedItem = selected;
Rory
@Rory: Thanks; I guess I wasn't clear. I understand everything in your answer but--maybe it's just me--doesn't that seem like a bug? In the contrived example I have in my original question, the fix (as you specified) is easy. But in a real application, the changing of the property could be happening ANYWHERE, most likely in a completely different form. It seems like a hack to have to handle something like INotifyPropertyChanged just to keep the ComboBox from pointing to a random item depending on the sort algorithm of the data source. It would seem to make binding in a ComboBox very fragile.
Dave
@Dave: Databinding in winforms has always felt like a bit of a bug to me, it was a nice concept with a very crappy implementation. Thankfully WPF and Silverlight are a lot better at it, but I guess thats not much comfort if you're stuck using winforms. Unfortunately I think the best solution to your problem is to just avoid the situation completely: don't update the sort field and avoid adding rows that might be inserted before existing ones. Sorry, I know it's not much help, but I can't think of another way to do it.
Rory
A: 

From an architecture perspective, the SelectedItem must be cleared when rebinding the DataSource because the DataBinder don't know if your SelectedItem will persist or not.

From a functional perspective, the DataBinder may not be able to ensure that your SelectedItem from you old DataSource is the same in your new DataSource (it can be a different DataSource with the same SelectedItem ID).

Its more an application feature or a custom control feature than a generic databinding process.

IMHO, you have theses choices if you want to keep the SelectedItem on rebind :

  • Create a reusable custom control / custom DataBinder with a persistance option which try to set the SelectedItem with all your data validation (using a DataSource / item identification to ensure the item validity)

  • Persist it specifically on your Form using the Form/Application context (like ViewState for ASP.NET).

Some controls on the .NET market are helping you by rebinding (including selections) the control from their own persisted DataSource if the DataSource is not changed and DataBind not recalled. That's the best pratice.

JoeBilly
A: 

The only promising solution I see at this time is to bind the combo box to a detached data source and then update it every time the "real" DataView changes. Here is what I have so far. Seems to be working, but (1) it's a total hack, and (2) it will not scale well at all.

In form declaration:

private DataView shadowView;

In form initialization:

this.comboBox1.DisplayMember = "Value";
this.comboBox1.ValueMember = "Key";
this.shadowView = new DataView(GlobalData.TheGlobalTable, null, "LastModified DESC", DataViewRowState.CurrentRows);
this.shadowView.ListChanged += new ListChangedEventHandler(shadowView_ListChanged);
this.ResetComboBoxDataSource(null);

And then the hack:

private void shadowView_ListChanged(object sender, ListChangedEventArgs e)
{
    this.ResetComboBoxDataSource((int)this.comboBox1.SelectedValue);
}

private void ResetComboBoxDataSource(int? selectedId)
{
    int selectedIndex = 0;
    var detached = new KeyValuePair<int, string>[this.shadowView.Count];
    for (int i = 0; i < this.shadowView.Count; i++)
    {
        int id = (int)this.shadowView[i]["ID"];
        detached[i] = new KeyValuePair<int, string>(id, (string)this.shadowView[i]["Name"]);
        if (id == selectedId)
        {
            selectedIndex = i;
        }
    }
    this.comboBox1.DataSource = detached;
    this.comboBox1.SelectedIndex = selectedIndex;
}

Must detach event handler in Dispose:

this.shadowView.ListChanged -= new ListChangedEventHandler(shadowView_ListChanged);
Dave