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:

<ComboBox Height="20" Width="100" 
ItemsSource="{Binding RoleListMembers}" 
SelectedItem="{Binding UserModel.Role}" />

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:

<ComboBox Height="20" Width="100"   
    SelectedValue="{Binding UserModel.RoleId}"
    SelectedValuePath="Id"
    ItemsSource="{Binding RoleListMembers}"
    />

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.

  • qwerty23

    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 works fine, but the problem is that I need an extra option for the ComboBox, something like “Please select” or “Undefined”, which shouldn’t appear in the drop down list but it should be setable by code.

    Is there any way to do this? I have tried making the Level propery nullable and using a converter on SelectedValue to convert “null” into the new option name, but it does not work, it just shows blank when Level is null.

    Any help would be appreciated.

    Thanks a lot!

  • http://www.daedtech.com/blog Erik Dietrich

    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 can set the value of your enum internally or pop a validation message or something if they haven’t selected an option.

    Hope this helps.

  • qwerty23

    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 different levels, it should show “Multiple”, but the value “Multiple” should not appear in the drop down list (basically it shouldn’t be in the ItemSource, I think).

    So the question is: Is there any way to make a ComboBox display a value which isn’t in its ItemSource?

    Thanks a lot!

  • http://www.daedtech.com/blog Erik Dietrich

    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) Start off with the “Multiple” value in your items source, and when the user clicks the dropdown, remove it from the collection of items (not sure how well this would work, but it’s a thought)
    (3) Define a custom control that inherits from Combo Box and extends its functionality. You can define a dependency property on it called SpecialValue or something and then roll this behavior into it. This is probably the approach that I’d take to automate the appearance behavior you want but leave the normal binding behavior intact.

    I apologize if the specifics are a little rough around the edges. It’s been about 4 months since I’ve done anything in WPF or XAML.

  • qwerty23

    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 wanted to use a DataGridComboBoxColumn and it does not have a SelectedIndex, only a SelectedValueBinding, so I had to use a DataGridTemplateColumn with a ComboBox in it. Anyway, It’s strange that such a simple feature requires so much customization.

    Thanks for the help!

  • qwerty23

    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

  • http://www.daedtech.com/blog Erik Dietrich

    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 doing two things that I generally really try to avoid: using enums and using value converters. With enums, I almost always favor objects instead. Imagine if you did that here — you could have a bunch of LogLevel objects that implement an interface or inherit from a base class and override ToString(). Then you could create an observable collection and bind to them directly, not needing to bother with value converters or enum parses or anything else. With value converters, I will use them for something that is a pure view concern, like converting a boolean to a visibility or an int to a width or pixel count or something, but absolutely never will I use them for anything resembling business logic or infrastructure. The converters are so divorced from the rest of your application flow that troubleshooting them in a nightmare. They should be simple enough that they could never, ever possibly fail in any way.

    My take, anyway. Hope it helps.