Excedrin headache #3.5.40128.1: Using combo boxes with the WPF DataGrid

If you’re working with the WPF DataGrid (January 2009 release, version 3.5.40128.1) and want to display a column of combo boxes (e.g. to select a value from a list of options) you may be in for a headache, especially if you want the available options to be supplied from the binding context. For example say you are displaying an editable table of orders, and each order is for a particular quantity of a particular product, and the products are selectable using combo boxes.

Here is a solution, the main element of which came from a posting on the Codeplex web site from someone called XIU for which I’m grateful. I thought I would add some more detail in case anyone else has this problem and wants a more cut and pasty solution.

Say you have a class called Product, each instance of which has an ID and a Description, and a class called Order, each instance of which has a Quantity and a Product ID (to be used like a foreign key to designate the order’s product).

These classes must implement the INotifyPropertyChanged interface. Here are the first few lines of each:

public class Product : INotifyPropertyChanged
{
  public int ID { ... }
  public string Description { ... }
  ...
}
 
public class Order : INotifyPropertyChanged
{
  public int ProductID { ... }
  public int Quantity { ... }
  ...
}

Create a simple ViewModel (i.e. binding context) and set the DataContext of the main window to an instance of this object:

namespace ComboBoxDemo
{
  public class ViewModel
  {
    public ObservableCollection<Order> Orders { get; set; }
    public ObservableCollection<Product> Products { get; set; }
 
    public ViewModel()
    {
      Products = new ObservableCollection<Product>();
      Products.Add(new Product(100, "Widget"));
      Products.Add(new Product(101, "Sprocket"));
      Products.Add(new Product(102, "Gadget"));
 
      Orders = new ObservableCollection<Order>();
      Orders.Add(new Order(102, 15));
      Orders.Add(new Order(102, 32));
      Orders.Add(new Order(100, 42));
    }
  }
}

Create a helper class using XIU’s solution:

namespace ComboBoxDemo
{
  public class DataGridComboBoxColumnWithBindingHack : DataGridComboBoxColumn
  {
    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
      FrameworkElement element = base.GenerateEditingElement(cell, dataItem);
      CopyItemsSource(element);
      return element;
    }
 
    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
      FrameworkElement element = base.GenerateElement(cell, dataItem);
      CopyItemsSource(element);
      return element;
    }
 
    private void CopyItemsSource(FrameworkElement element)
    {
      BindingOperations.SetBinding(element, ComboBox.ItemsSourceProperty,
        BindingOperations.GetBinding(this, ComboBox.ItemsSourceProperty));
    }
  }
}

This is a variant of the DataGridComboBoxColumn class which handles binding differently to allow the ItemsSource property to have access to the binding context. Now put it all together with the following XAML:

<Window x:Class="ComboBoxDemo.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:wpf="http://schemas.microsoft.com/wpf/2008/toolkit"
  xmlns:local="clr-namespace:ComboBoxDemo"
  Title="Window1" Height="300" Width="300">
  <wpf:DataGrid ItemsSource="{Binding Orders}" AutoGenerateColumns="False">
    <wpf:DataGrid.Columns>
      <wpf:DataGridTextColumn Header="Quantity" Binding="{Binding Quantity}" />
      <local:DataGridComboBoxColumnWithBindingHack
        Header="Product"
        SelectedValueBinding="{Binding ProductID}"
        SelectedValuePath="ID"
        DisplayMemberPath="Description"
        ItemsSource="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}, Path=DataContext.Products}" />
    </wpf:DataGrid.Columns>
  </wpf:DataGrid>
</Window>

The SelectedValueBinding attribute indicates the Order property containing the foreign key designating the product (i.e. which value should be edited by the combo box). The SelectedValuePath attribute indicates which property within the product object contains the product ID, and the DisplayMemberPath attribute indicates which property should actually be displayed in the grid. Finally the ItemsSource attribute indicates how to get the list of products by navigating to the parent window object and looking up the list of products from the parent window’s binding context.

Here is the complete project source as a downloadable zip file.

I hope this saves someone some time, and that it’s easier to do all of this in future versions of the WPF Toolkit. If anyone has a better solution to this problem, please let me know.

12 Comments

  1. The problem here seems to be that the DataGridColumn doesn’t get placed in the UserControl’s tree, so the natural instinct for solving this problem – using an Ancestor RelativeSource binding – doesn’t work. I think this is pretty plainly a bug.

    I’ve solved this problem in my application a bit more directly, by setting the DataGridComboBoxColumn’s ItemsSource binding in the code-behind of the control in which the DataGrid is contained. Not clean M-V-VM-wise, but very easily understood, I think.

  2. Thanks a lot !!! It works great.
    The bug has not been fixed in the latest release of the WPF Toolkit (June 2009, 25th), so I’ll keep using your solution which is MVVM compliant.

    Keep up the good work :-)

  3. Thanks for this. This problem has caused me a lot of pain over the last day or so, glad I finally stumbled upon your solution.

  4. Elegant work-around that you clearly documented. My only suggestion is to include the “using” statements in the code block. I picked them up from the zip file, though, so not a big deal.

  5. Question!

    I’ve implented this on my project but the DropDown List is not displaying the description for each element on it (I only got the reference as ComboBoxDemo.Product). However, if I select 1 of those elements, I got the description right on the cell.

    Is this a bug? How can I fix it?

  6. I’ll add my thanks here as well. This is a really confusing area of WPF and your example was just what I needed after struggling (guessing) on my own for a few hours.

  7. Your suggested approach works, but I still get binding errors in the output.

    This behavior actually is no bug, this is by design.
    Jacek Sieka came up with the proper solution.

Leave a Reply

Your email address will not be published. Required fields are marked *