All posts tagged 'WPF'

.NET Musings

Wandering thoughts of a developer, architect, speaker, and trainer

NAVIGATION - SEARCH

The WPF ComboBox - SelectedItem, SelectedValue, and SelectedValuePath with NHibernate

Recently, I received a question from one of my twitter tribemates on using the ComboBox in WPF with objects, and it centered around the difference between the SelectedValue, SelectedValuePath, and SelectedItem properties. This post will go through the differences, and how they are used, and also do a little review on databinding in WPF.

To set this up, let's define Category objects and Product objects like this:

Category
--ID
--CategoryName
--Products (An IList)

Product
--ID
--ProductName
--CategoryParent (A Category Object)

In the detail form for the Product, you want to have a ComboBox that has as its ItemsSource the list of Categories. When a Product is selected, the ComboBox should change the selection to align with the CategoryParent property of the Product. Changing the Selected Item in the ComboBox should update the CategoryParent property of the Product.

The way I like to set up detail forms is to have my Selector ComboBox (in this case for Products) at the top, and then the detail below in a Grid panel. This makes the databinding easier, since elements will walk up the element tree to find their datacontext.

If we define out Products ComboBox like this (I've eliminated the attached properties and other styling from these samples for readability):

<ComboBox Name="ProductsList" DisplayMemberPath="ModelName">

By adding the Grid Panel for the Product Details at the same level (or lower) than the ProductsList ComboBox, we can set the DataContext very simply using ElementBinding:

<Grid Name="ProductDetailsGrid" DataContext="{Binding ElementName=ProductsList,Path=SelectedItem}">

Now, for each element in the ProductDetailsGrid, we simply create a TextBox and set the Binding Path:

<TextBox Text="{Binding Path=ProductName}">

(Rember for TextBoxes, the UpdateSourceTrigger default is LostFocus. If you want the trigger to fire as soon as the contents are changed, you need to set the property value to "PropertyChanged".)

Now for the meat of this post - the ComboBox for the Category List. Creating a ComboBox that displays the list is quite simple.

<ComboBox Name="CategoryList" DisplayMemberPath="CategoryName">

Let's tackle the second issue (changing the CategoryParent value of the selected Product) first. The SelectedItem property, when bound to the correct property of a Product, will allow the user to make updates. This is not news, as it works very similarly in WinForms.

<ComboBox Name="CategoryList" DisplayMemberPath="CategoryName" SelectedItem="{Binding Path=CategoryParent}">

What is surprising, is when the selected Product changes, the list doesn't update if you are using an ORM tool (like NHibernate or Castle Active Record). The reason for this is that the CategoryParent property holds an instance of a category object wrapped in a proxy (I use lazy initialization), and the CategoryList holds a set of instances of Category objects. These end up not being the same in terms of the default object Equals method.

There are a couple of solutions to this. Either overload the Equals method in your objects (also remembering to overload GetHashCode), or use SelectedValue and SelectedValuePath.

If you have access to the code and can override Equals/GetHashCode, that is the cleaner implementation.
  1: public virtual bool Equals(Category obj) {
  2:   if (ReferenceEquals(null, obj))
  3:     return false;
  4:   if (ReferenceEquals(this, obj))
  5:     return true;
  6:   return obj.ID == ID;
  7: }
  8: public override bool Equals(object obj) {
  9:   Category cat = obj as Category;
 10:   return cat != null && Equals(cat);
 11: }
 12: public override int GetHashCode() {
 13:   unchecked
 14:   {
 15:     return (ID * 397);
 16:   }
 17: }
 18: 
The unckecked keyword makes sure that ID*397 will get truncated if it exceeds Int32.MaxValue. Certainly not the best algorithm for GetHashCode, but it suffices for this post.

If you can't override Equals, then you will have to use the SelectedValue/SelectedValuePath method for synchronizing the value of the CategoryParent to the item in the Category List. This works as well, but it obviously not nearly as encapsulated as changing the POCOs themselves.

The SelectedValue property defines which property of the bound data object will be used to get the value. The SelectedValuePath defines the property of the List Object to match. So, in our example, we end up with:

<ComboBox Name="CategoryList" DisplayMemberPath="CategoryName" SelectedItem="{Binding Path=CategoryParent}"
SelectedValue="{Binding Path=CategoryParent.ID}"
SelectedValuePath="ID">

Note that you have to keep the SelectedItem property if you want to be able to update the Product property CategoryParent.

Hopefully, this clears up the mystery around why SelectedItem doesn't work as expected with ORM layers like NHibernate.

Happy Coding!
Managed Windows Shared Hosting by OrcsWeb