Sunday, August 19, 2018

ComboBox Gotcha #2

Recently, I participated in an MSDN Forum thread about problems with ComboBoxes, SelectedValue and SelectedIndex. It reminded me of a "gotcha" that I knew about a long time ago, but had forgotten about. I haven't been doing any development on Windows Forms apps in a long time (I have been doing only back-end server side stuff for more than a few years now), although many years ago I was responsible for a pretty extensive Windows Forms "Framework" for our developers to use for all of our company's applications. So, I knew about this then (and I'm sure that I built it into the Framework's base classes DataBinding methods). But, I digress ... let's get to a description of the problem and how to make sure it doesn't happen to you!

Say that in your application, you need to do something with the SelectedValue of a ComboBox. So, you create an event handler for either the .SelectedIndexChanged event or the .SelectedValueChanged event. But it doesn't work the way you expect it to and you're getting exceptions sometimes. What could be the problem? Let's look at the result of some debugging code placed in each of those event handlers to try and troubleshoot the problem:

Here's the debugging code:

private void cboCustomer_SelectedValueChanged(object sender, EventArgs e)
{
Console.WriteLine($"Value Changed: SelectedIndex is {cboCustomer.SelectedIndex}");
Console.WriteLine($"Value Changed: SelectedValue is {cboCustomer.SelectedValue}, is a {cboCustomer.SelectedValue.GetType()}");
Console.WriteLine("");
// the rest of your code is here
}
private void cboCustomer_SelectedIndexChanged(object sender, EventArgs e)
{
Console.WriteLine($"Index Changed: SelectedIndex is {cboCustomer.SelectedIndex}");
Console.WriteLine($"Index Changed: SelectedValue is {cboCustomer.SelectedValue}, is a {cboCustomer.SelectedValue.GetType()}");
Console.WriteLine("");
// the rest of your code is here
}

And here are the results:

Value Changed: SelectedIndex is 0
Value Changed: SelectedValue is System.Data.DataRowView, is a System.Data.DataRowView

Index Changed: SelectedIndex is 0
Index Changed: SelectedValue is System.Data.DataRowView, is a System.Data.DataRowView

Value Changed: SelectedIndex is 0
Value Changed: SelectedValue is System.Data.DataRowView, is a System.Data.DataRowView

Value Changed: SelectedIndex is 0
Value Changed: SelectedValue is System.Data.DataRowView, is a System.Data.DataRowView

Index Changed: SelectedIndex is 0
Index Changed: SelectedValue is System.Data.DataRowView, is a System.Data.DataRowView

Value Changed: SelectedIndex is 0
Value Changed: SelectedValue is 01, is a System.String

This is obviously, not a good thing ... the events are firing way too many times. And why is the SelectedValue a DataRowView at first (in my example I am using a DataTable as the Combo's DataSource)?  In fact, these events should not be firing at all at this time ... we have simply set up the databinding for the ComboBox.  Wow! What did we do wrong?

Here's the databinding code (remember that this is BAD CODE!)

this.bsCustomers = new BindingSource();
this.bsCustomers.DataSource = this.oDataFromXML.Tables["Customer"];

this.cboCustomer.SelectedIndexChanged += cboCustomer_SelectedIndexChanged;
this.cboCustomer.SelectedValueChanged += cboCustomer_SelectedValueChanged;

this.cboCustomer.DataSource = this.bsCustomers;
this.cboCustomer.DisplayMember = "Last Name";
this.cboCustomer.ValueMember = "CustomerID";

The problem exists because the DataSource is set first. The subsequent statements setting the DisplayMember and ValueMember each fire the events (because now the SelectedValue Properties are changed by setting those Members).

There is an easy way to fix this, and perhaps you've already guessed it!  Set the DataSource *after* setting the DisplayMember and ValueMember.

this.bsCustomers = new BindingSource();
this.bsCustomers.DataSource = this.oDataFromXML.Tables["Customer"];

this.cboCustomer.SelectedIndexChanged += cboCustomer_SelectedIndexChanged;
this.cboCustomer.SelectedValueChanged += cboCustomer_SelectedValueChanged;

this.cboCustomer.DisplayMember = "Last Name";
this.cboCustomer.ValueMember = "CustomerID";
this.cboCustomer.DataSource = this.bsCustomers;

But wait ... what's going on? Now the code in the event handlers throws an exception!

Value Changed: SelectedIndex is -1
Exception thrown: 'System.NullReferenceException' in WindowsApplication1.exe

That's because, now that we've done the databinding *correctly*, initially nothing is selected (this only happens once) and consequently the SelectedIndex is -1 and the SelectedValue will be null.  You *must* have code to check that SelectedIndex is not negative! Especially if you databind the ComboBox to a DataTable that initially has no data in it! When there *is* data, the first item will then be automatically selected and the SelectedIndex will be 0 ... but if there is *no* data, obviously nothing can be selected. The SelectedIndex will be -1 to indicate that.

private void cboCustomer_SelectedValueChanged(object sender, EventArgs e)
{
Console.WriteLine($"Value Changed: SelectedIndex is {cboCustomer.SelectedIndex}");
if (cboCustomer.SelectedIndex > -1)
{
Console.WriteLine($"Value Changed: SelectedValue is {cboCustomer.SelectedValue}, is a {cboCustomer.SelectedValue.GetType()}");
Console.WriteLine("");
// the rest of your code is here
}
}
private void cboCustomer_SelectedIndexChanged(object sender, EventArgs e)
{
Console.WriteLine($"Index Changed: SelectedIndex is {cboCustomer.SelectedIndex}");
if (cboCustomer.SelectedIndex > -1)
{
Console.WriteLine($"Index Changed: SelectedValue is {cboCustomer.SelectedValue}, is a {cboCustomer.SelectedValue.GetType()}");
Console.WriteLine("");
// the rest of your code is here
}
}

And the results now:

Value Changed: SelectedIndex is -1
Value Changed: SelectedIndex is 0
Value Changed: SelectedValue is 01, is a System.String

Index Changed: SelectedIndex is 0
Index Changed: SelectedValue is 01, is a System.String

In a similar vein, I wrote a blog post back in 2012 with the title "ComboBox Gotchas". But, it was about a totally different problem ... a ComboBox that is in sync with other controls on the Form, when it's not supposed to be. See it here, if you're interested: https://geek-goddess-bonnie.blogspot.com/2012/12/combobox-gotchas.html