WPF Combo Boxes
I thought I’d put together a quick post today on something that annoyed me and that I found unintuitive: binding with the WPF combo box. I’m doing some development following MVVM, and a situation came up in which I had a view model for editing a conceptual object in my domain. Let’s call it a user for the sake of this post, since the actual work I’m doing is nominally proprietary.
So, let’s say that user has a first name and a last name and that user also has a role. Role is not an enum or a literal, but an actual, conceptual reference object. In the view model for a customer edit screen, I was exposing a model of the user domain object for binding, and this model had properties like first name and last name editable via text box. I now wanted to add a combo box to allow for editing of the role by selecting one choice from a list of valid roles.
Forgetting the code for the presentation tier, I did this in the XAML:
Now, I’ve had plenty of occasions where I’ve exposed a list from a view model and then a separate property from the view model called “Selected Item.” This paradigm above would faithfully set my SelectedItem property to one of the list members. But here, I’m doing something subtly different. The main object in the view model–its focus–is UserModel. That is, the point of this screen is to edit the user, not roles or any other peripheral data. So, what I’m actually doing here is trying to bind a reference in another object to one of the items in the list.
What I have above didn’t work. And after a good bit of reading and head scratching, I figured out why. SelectedItem tries to find whatever it’s bound to in the actual list. In the case where I have a list in my view model and want to pick one of the members, this is perfect. But in the case where the list of roles contains objects that are distinct from the user’s role reference, this doesn’t work. The reason it doesn’t work, I believe, is that SelectedItem operates on equality. So if I were to create a list of roles and then assign one of them to the user model object, everything would be fine, since object equals defaults to looking for identical references. But in this case, where the list of roles and the user’s role are created separately (this is done in the data layer in my application) and have nothing in common until I try to match my user role to the list of roles, reference equals fails.
As an experiment, I tried the following:
public class Role
{
public int Id { get; set; }
public override bool Equals(object obj)
{
return Equals(obj as Role);
}
public bool Equals(Role role)
{
return role != null && role.Id == Id;
}
}
After I put this code in place, viola, success! Everything now worked. The combo box was now looking for matching IDs instead of reference equals. I packed up and went home (it was late when I was doing this). But on the drive home, I started to think about the fact that two roles having equal IDs doesn’t mean that they’re equal. ID is an artificial database construct that I hide from users for identifying roles. ID should be unique, and I have a bunch of unit tests that say that it is. But that doesn’t mean that equal IDs means conceptual equality. What if I somehow had two roles with the same ID but different titles, like, say, if I was allowing the user to edit the role title. If for some reason I wanted to compare the edited value with the new one using Equals(), I wouldn’t want the edited and original always to be equal simply because they shared an ID.
I figured I could amend the equals override, but I’m not big on adding code that I’m not actually using, and this is the only place I’m using Equals override. So I went back to the drawing board and read a bit more about the combo box. What I discovered was a couple of additional, rather unfortunately named properties: SelectedValue and SelectedValuePath. Here is what the amended working version looked like without overriding Equals:
ItemsSource is the same, but instead of a SelectedItem, I now have a “SelectedValue” and a “SelectedValuePath”. SelectedValue allows you to specify the binding target by a property of it, and SelectedValuePath allows you to specify which property of the members of ItemsSource should match SelectedValue.
So what the above is really saying is “I have a list of Roles. The role that’s selected is going to be whichever role in the list has an ID property that matches UserModel’s role ID.” And by default, when you change which value is selected, the UserModel’s “RoleId” gets updated with the new selection.
This actually somewhat resembles what I remember from doing Spring and JSP many moons ago, but there’s a little too much rust and probably too many JDKs and whatnot released between then and now for me to know that it’s current. When you actually get down to the nitty gritty of what’s going on, it is intuitive here. I just think the control’s naming scheme is a bit confusing. I would prefer something that indicated the relationship between the item and the list.
But I suppose lack of familiarity always breeds confusion, which in turn breeds frustration. Maybe now that I took the time to understand the nitty gritty instead of just copying what had worked for me in previous implementations, I’ll warm up to the naming scheme.
Hi, I have a problem that has been bothering me for days. I made up this simple example to illustrate it. Hopefully you can give me a hint. ————————————————————————————————————————————– public enum Levels { Info, Debug, Error, Warning, Fatal } ——————————————– class Logger { private Levels level; public Levels Level { get { return level; } set { this.level = value; } } } ——————————————– class DataContext { private Logger logger; public DataContext() { logger = new Logger(); logger.Level = Levels.Info; } public Logger Logger { get { return logger; } } } ——————————————– ——————————————– combobox1.DataContext = new DataContext(); ————————————————————————————————————————————– This… Read more »
Off the top of my head, the way I would handle this is not to bind directly to the enum (in general, my preference is to avoid implicit conversions such as the one from the enum to the string values). In your data context class, you could have an Observable Collection of strings that you expose for binding. In your xtor, you could add “Please Select” to the collection and then iterate over your enum values, adding them as well. Then, you expose a string called “SelectedValue” and bind the combo box’s selected value to it. In its setter, you… Read more »
Thanks for the reply! Essentially, that is more or less what I am doing in the real application. I only gave you a simplified example. The problem is that in this way, the “Please Select” option would also appear in the drop down list when the user clicks the ComboBox and I don’t want that. Again, “Please select” is just an example. What I want in the real app is: The ComboBox sets the Level for multiple Loggers at the same time. When the Loggers behind all have the same Level, it should show that value, but when they have… Read more »
Oh, I see the idea — having a combo box display a non-selectable value in certain, special states. Again, off the top of my head, I can think of three ways to approach this: (1) If the only “odd” scenario is going to occur when the list loads, you could have the combo box bind to your correct item source, set its IsEditable property to true and its text to “Multiple”. This would get you what you want, but the combo box would be editable (meaning you could type in it) and that may not be what you want. (2)… Read more »
Thank for the reply. I tried all those approaches at some point but each one got stuck somewhere. What I ended up doing is something similar to this: ——————————————————————————————– ——————————————————————————————– class testConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { string valueString = value as string; if(valueString == null) return 0; return (int)(Enum.Parse(typeof(Levels), valueString)); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { int valueInt = (int)value; if(valueInt == 0) return null; return (Levels)valueInt; } #endregion } ——————————————————————————————– It works, but it’s not a perfect solution because i… Read more »
PS: what I meant there was: When I want the “Multiple” option, I set the Level property to null, and “Multiple” does not appear in the list because it is Collapsed
I agree that it’s strange not to have some simple out of the box solution…. Here’s something I coded up that’s quick and dirty but functional (if I were doing this in an actual project, I would probably abstract this to a custom control called “DefaultValueComboBox” or something): MainWindow.xaml MainWindow.xaml.cs MainWindowViewModel.cs This actually works out of the box, having a default value that disappears when the user starts making selections. If you want different behavior in any way, you can tweak the GUI a bit. Working with what you posted, it’s sort of hard for me to advise since you’re… Read more »