Introduction
This article takes a deep look at a commonly used .NET construct (DataBinding) step by step. First, the article sets up a simple program that binds an array of objects to a ComboBox
using the DisplayMember
and ValueMember
properties. Next, it shows how this can often fail. After that, we will see ways to fix the problem, and see why there is probably a bug in the control, in Visual Studio, in something.
Background
I discovered this problem quite some time ago, but was never able to determine why it occurred, because it was part of a larger program. So, I decided to write a specific program and article which provides specific details.
- How to bind an object's values directly to a
ComboBox
. - What problems, errors, and failures occur.
- Why those problems often occur.
Using the Code
While reading the article, you will see that the code goes through many slight modifications to show you along the way what happens. The attached code is the one which implements the Array
and the ToString()
override on the Animal
class. This will be clearer after reading the article.
The final code is a basic Windows Forms project with one additional class, kept very simple in an effort to create a clear sample of what is happening:
Here is the Animal
class:
class Animal
{
private string commonName;
private string species;
private int speciesId;
static Random rnd = new Random();
public Animal(string inSpecies, string inCommonName)
{
this.speciesId = rnd.Next();
species = inSpecies;
commonName = inCommonName;
}
public int SpeciesId
{
get
{
return speciesId;
}
}
public string Species
{
get
{
return species;
}
}
public string CommonName
{
get
{
return commonName;
}
}
}
Animal Class Details
The Animal
class makes it easy to bind our list of animals to the ComboBox
. In our form code where we have the ComboBox
, we will create an array of Animal
s and then bind them to our ComboBox
. It will look like the following:
public partial class Form1 : Form
{
Animal[] allAnimals = new Animal[10];
public Form1()
{
InitializeComponent();
InitAnimals();
comboBox1.DisplayMember = "CommonName";
comboBox1.ValueMember = "SpeciesId";
comboBox1.DataSource = allAnimals;
}
private void InitAnimals()
{
allAnimals[0] = new Animal("Fidelis Caninus", "Dog");
allAnimals[1] = new Animal("Felinus Catticus", "Cat");
allAnimals[2] = new Animal("Elephantos Largus", "Elephant");
}
}
This code will produce an application that binds the animal objects to the ComboBox
, and looks like the following:
Second Image: Shows an Empty Item
Please also note that the second image shows the program as it looks when it first starts. The nice thing is that this ComboBox
has an empty item. In other words, the user hasn't selected a choice yet. This may not seem important yet, but it will when we attempt to solve the problem below. So, for now, just keep it in mind.
Now, let's add an event handler so that choosing one of those items does something. In Visual Studio, make sure the ComboBox
is selected, then switch over to the Events view on the Properties dialog of your form, and add the SelectedIndexChanged
event. You'll get a method that looks something like:
private void comboBox1_SelectedIndexChanged_1(object sender, EventArgs e)
{
Animal currentAnimal = (Animal)comboBox1.SelectedItem;
MessageBox.Show(this,currentAnimal.Species);
}
When you run the program and choose an item in the combo box, the event handler fires, gets the currently selected item, casts it to an Animal
object, and displays the species name of the animal. It'll look like the following, depending upon which item you select:
Why do We Cast the Item?
We cast the item that comes back from the ComboBox
, because we told it to store that item in the ComboBox
's list, but the ComboBox
simply stores it as an object
- the mother of all objects. Since we have specific properties that we want from our specific (Animal
) object, we cast it back to its real type.
Seems to Work Fine
So, you may notice that it seems to work fine. But there is a problem that some of you may have noticed. The problem: there is an array with some empty elements. I defined the array as a size 10, but I only new
ed up three Animal
objects.
Works: Unless You Change the Order
Okay, you see that the code really does work, so does it matter? For now, let's suppose that it does not matter. But, let's change one line of code and show that it will cause the code to break.
public Form1()
{
InitializeComponent();
InitAnimals();
comboBox1.DataSource = allAnimals;
comboBox1.DisplayMember = "CommonName";
comboBox1.ValueMember = "SpeciesId";
}
The Crash
Notice that the only change I made was in moving the line where I set the DataSource: comboBox1.DataSource = allAnimals;
. Previously, I set it after I set the DisplayMember
and ValueMember
. Now, I set it before. Now, when you attempt to run the program, it will instantly crash. You will see something like the following:
Why Does it Crash?
To find out why the program crashes, I stepped through the code with the debugger. The following exception gets thrown:
But, that doesn't make much sense. Why is it telling me that the value cannot be null? My value isn't null. It's a perfectly good string, which points to the ValueMember
SpeciesId
of my Animal
class. Besides, it works if I put this line later.
Microsoft Documentation On Databinding?
Maybe Microsoft even claims that you have to do this in this order? Actually, they don't. As far as any documentation is concerned, what I've created should be fine. But, it's not.
1st Workaround Solution
The first and most obvious solution is: Don't do that. In other words, just set the DataSource
member last and forget about it. But that sounds like a totally magical solution, so let's look for another.
2nd Workaround Solution: Handle (Throw Away) the Exception
Yeah, let's just throw the exception away. I simply wrap the line in a try
...catch
... which handles the ArgumentNullException
. It looks like the following:
try
{
comboBox1.ValueMember = "SpeciesId";
}
catch (ArgumentNullException ex)
{
}
Does Try...Catch...Work?
It does work. The application stars and everything looks just fine, until I click the ComboBox
. See the next two images:
Odd Display Values
What is going on? Why do I now have those odd display values? Where could those odd display values be coming from?
Closer Look at the Animal Class
Hey, remember the ID that I generate in the Animal
class? I did that to emulate a value you might get back from a database or something like that. The code that does that looks like this:
public Animal(string inSpecies, string inCommonName)
{
this.speciesId = rnd.Next();
species = inSpecies;
commonName = inCommonName;
}
I do some more analysis and determine that, yes, that is where those values are coming from. But why do they get set as the DisplayValue
? And, what happens if I do choose one of those values? The program works as you would (possibly) expect. It looks like this:
3rd Workaround Solution
Okay, how about we try something else? Let's not even set the stupid (yes, I said stupid) comboBox1.ValueMember
. Let's just ignore it. Let's just comment that line of code out and try again. Here's the code and the results:
Again, everything looks fine at first. Then I drop the list and...what? Now, it lists my namespace.className as the DisplayMember
. Crazy! But, it still works, if you can call that working.
A Clue and ToString()
Well, at least this leads me to a clue. Now, I'm thinking, hey, this is using the default value of my Animal
class ToString()
method. Let's override the ToString()
method and see what I can do. So, I go the Animal
class and add the following override method. Notice that the auto-robot-coder (Visual Studio Helper) attempts to add the return base.ToString();
line to my code, but I comment it out. I don't want to do the default behavior.
Instead, I tell it to return the commonName
property of my Animal
class.
public override string ToString()
{
return this.commonName;
}
It Works, But Wait! Should it?
Here's what it looks like now, when I run it.
So it works, but I'm not convinced it should. Microsoft tells you that you need to set all three items:
comboBox1.DisplayMember = "CommonName";
comboBox1.ValueMember = "SpeciesId";
comboBox1.DataSource = allAnimals;
But, remember, now I am only setting the DisplayMember
. I never even set the ValueMember
, because I commented it out. Why don't I need to any more? How can the control distinguish the correct item / value?
Yet Another Workaround Solution
Lose the Array, Use a List (Collection)
So, maybe it's all related to using this array, which has some elements which are still null? Okay, I'll change my code so it uses a Collection. Here's the code:
List<animal> allAnimals = new List<animal>();
public Form1()
{
InitializeComponent();
InitAnimals();
comboBox1.DataSource = allAnimals;
comboBox1.DisplayMember = "CommonName";
comboBox1.ValueMember = "SpeciesId";
}
private void InitAnimals()
{
allAnimals.Add(new Animal("Fidelis Caninus", "Dog"));
allAnimals.Add(new Animal("Felinus Catticus", "Cat"));
allAnimals.Add(new Animal("Elephantos Largus", "Elephant"));
}
I also altered the Animal
class by removing the previously added ToString()
.
So, now I run the thing and the first thing I see is:
Yes, I see that before I see the main form. Obviously, now the SelectedItemChanged
event of the combobox is running immediately after I start the program. Why? Where else is this explained? Anywhere? Anyone?
Right after that is displayed, the main form appears and looks like the following:
ComboBox Blank Item: It's Gone!
Remember when I asked you to notice that there was a blank item in the ComboBox
? I did. Up at the top of this article, I said, "Hey, look, there's a blank item in the combobox, since the user hasn't selected one yet." Well, I had you notice that, so we cold talk about it now. You see, it is gone now.
Hey, you may think it is no big deal. We can get around that. I know, but why isn't anything consistent with this whole binding thing? Isn't this supposed to be easy?
Hoping to Help
I'm hoping to help someone who bumps up against this problem in various places.
History
- 02.04.2010 - Posted first version of this article and code.