How To: Use an Editable DataGrid ComboBox Column

Nov 2, 2008 at 10:17 PM
Edited Nov 6, 2008 at 11:44 AM
I'm posting this solution as both an example for people who need this behavior and as a question for those of you who may know of a better way to accomplish this.  Built-in support for this scenario would be appreciated as well.

Problem description:

A DataGrid is being used in a WPF application to provide a convenient way for end-users to edit data.  The value of one particular column in the grid is constrained to a set of allowable values, although these values are only recommendations.  An end-user must be able to add new values on-the-fly.

Solution:


Update: Read further into this thread for a better solution that makes use of an actual DataGridComboBoxColumn and the DataGrid.CellEditEnding event instead of a DataGridTemplateColumn and the ComboBox.PreviewLostKeyboardFocus event, which is used in the following example.


A common approach for constraining a column's value to an allowable set is to use a DataGridComboBoxColumn, which can provide a convenient drop-down list of data-bound choices.  However, it doesn't appear to support the behavior that is required in this scenario (see the Note at the end of this post for more information).

The ComboBox.IsEditable property can be set to True to have a ComboBox act as both a TextBox and a drop-down list simultaneously.  But when the ComboBox is data-bound (both the selected item and the drop-down list), entering custom text will not cause a new item to be added to the data-bound collection, so it must be done manually.  In other words, if I enter 'Joe' in a ComboBox that is bound to a list of people, which doesn't contain the value 'Joe', then the value 'Joe' is not going to be added to the drop-down list automatically.  So if a binding is set for the SelectedValue or SelectedItem properties, or if SelectedValuePath is set, then the new value will simply be discarded when the control loses focus because the binding cannot find a corresponding value in the ItemsSource.

To enable this scenario we have to ditch DataGridComboBoxColumn and use DataGridTemplateColumn instead.  This gives us more control over the ComboBox so that we can manually insert new values into the data-bound collection when they are entered into the text area.

The following XAML binds a DataGrid to an ObservableCollection of Pair instances (Pair is a custom type and its source code follows).  The DataGrid contains a DataGridTextColumn bound to the Pair.Name property, a regular DataGridComboBoxColumn that does not allow you to add custom items and is bound to the Pair.Value property, and a DataGridTemplateColumn that meets the requirements of this scenario.  The XAML also contains a read-only DataGrid that is bound to the same data source as the editable DataGrid to prove that the underlying Pair collection is being updated when custom values are entered.

<Window x:Class="WpfApplication1.Window1"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:toolkit="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"

       xmlns:local="clr-namespace:WpfApplication1"

       Title="Editable DataGrid ComboxBox Column Sample" Height="320" Width="380"

       Loaded="Window_Loaded">

  <Window.Resources>

    <ObjectDataProvider x:Key="items"/>

    <ObjectDataProvider x:Key="lookup"/>

  </Window.Resources>

 

  <StackPanel>

    <TextBlock Margin="5">Editable Grid:</TextBlock>

    <toolkit:DataGrid ItemsSource="{Binding Source={StaticResource items}}"

                     AutoGenerateColumns="False">

      <toolkit:DataGrid.Columns>

        <toolkit:DataGridTextColumn Header="Name" Binding="{Binding Name}"/>

 

        <!-- This column shows the actual value of the item but new values cannot be added -->

        <toolkit:DataGridComboBoxColumn Header="Allowable Value" Width="*"

                                       SelectedItemBinding="{Binding Value}"

                                       ItemsSource="{Binding Source={StaticResource lookup}}" />

 

        <!-- This column allows an end-user to edit the collection of allowable values

             by simply typing a new value into the column  -->

        <toolkit:DataGridTemplateColumn Header="Editable Value" Width="*">

          <toolkit:DataGridTemplateColumn.CellTemplate>

            <DataTemplate>

              <TextBlock Text="{Binding Value}"/>

            </DataTemplate>

          </toolkit:DataGridTemplateColumn.CellTemplate>

          <toolkit:DataGridTemplateColumn.CellEditingTemplate>

            <DataTemplate>

 

              <ComboBox IsEditable="True"

                       SelectedItem="{Binding Value}"

                       ItemsSource="{Binding Source={StaticResource lookup}}"

                       PreviewLostKeyboardFocus="ComboBox_PreviewLostKeyboardFocus" />

 

            </DataTemplate>

          </toolkit:DataGridTemplateColumn.CellEditingTemplate>

        </toolkit:DataGridTemplateColumn>

      </toolkit:DataGrid.Columns>

    </toolkit:DataGrid>

 

    <!-- This grid binds to the same collection as the grid above to prove that the

         changes actually exist in the underlying collection -->

    <TextBlock Margin="5">Read-Only Grid:</TextBlock>

    <toolkit:DataGrid ItemsSource="{Binding Source={StaticResource items}}"

                     IsReadOnly="True" />

  </StackPanel>

</Window>

<!--EndFragment-->

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Input;

 

namespace WpfApplication1

{

  /// <summary>

  /// Interaction logic for Window1.xaml

  /// </summary>

  public partial class Window1 : Window

  {

    public Window1()

    {

      InitializeComponent();

    }

 

    private void Window_Loaded(object sender, RoutedEventArgs e)

    {

      ObjectDataProvider lookup = (ObjectDataProvider) FindResource("lookup");

      lookup.ObjectInstance = new ObservableCollection<string>()

      {

        "Value 1",

        "Value 2",

        "Value 3"

      };

 

      ObjectDataProvider items = (ObjectDataProvider) FindResource("items");

      items.ObjectInstance = new ObservableCollection<Pair>()

      {

        new Pair() { Name = "Pair 1", Value = "Value 1" },

        new Pair() { Name = "Pair 2", Value = "Value 2" },

        new Pair() { Name = "Pair 3", Value = "Value 3" }

      };

    }

 

    private void ComboBox_PreviewLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)

    {

      ComboBox box = (ComboBox) sender;

 

      if (box.SelectedIndex < 0 && box.Text.Length > 0)

      {

        IList<string> items = (IList<string>) box.ItemsSource;

        items.Add(box.Text);

        box.SelectedValue = box.Text;

      }

    }

  }

 

  public class Pair

  {

    public string Name { get; set; }

    public string Value { get; set; }

  }

}

<!--EndFragment-->
The custom behavior that we need here is written in the PreviewLostKeyboardFocus event handler.  The handler first checks that there's no selected item (box.SelectedIndex < 0) and then checks that the user has entered some text, and if so creates a new item (items.Add(box.Text);) and selects it (box.SelectedValue = box.Text;).

Note: Although DataGridComboBoxColumn does not provide an IsEditable property that you can set, it does allow you to set it by creating a custom EditingElementStyle, as mentioned in the work item's comments.  Howver, in trying to implement this scenario using DataGridComboBoxColumn with an EditingElementStyle and two Setters, one for IsEditable and one for the PreviewLostKeyboardFocus or LostFocus event, it seems impossible to update the actual value in the binding.  Setting the SelectedValue (or even SelectedIndex) like in the example above does nothing.  Setting the value on the current DataContext (by casting it to the expected type; in this case, Pair) will in fact select the appropriate item in that column but the binding is never updated; i.e., the Allowable Value column from the example above does not reflect the change when a custom value is entered into the Editable Value column even though that column does in fact retain the value.  The templated approach used in the example above does not exhibit this broken behavior.

- Dave
Coordinator
Nov 5, 2008 at 3:32 AM

I am not sure why DataGridComboBoxColumn didnt work for you but, the following XAML definition with exact same implementation of ComboBox_PreviewLostKeyboardFocus and Window_Loaded worked fine for me....

<Window x:Class="WpfApplication13.Window2"

                     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

                     xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"

                     Title="Window2" Height="300" Width="300" Loaded="Window_Loaded">

        <Window.Resources>

                <ObjectDataProvider x:Key="items"/>

                <ObjectDataProvider x:Key="lookup"/>

        </Window.Resources>

        <StackPanel>

                <TextBlock Margin="5">Editable Grid:</TextBlock>

                <toolkit:DataGrid ItemsSource="{Binding Source={StaticResource items}}" AutoGenerateColumns="False">

                        <toolkit:DataGrid.Columns>

                              <toolkit:DataGridTextColumn Header="Name" Binding="{Binding Name}"/>

                              <toolkit:DataGridComboBoxColumn Header="Allowable Value" Width="*" SelectedItemBinding="{Binding Value}" ItemsSource="{Binding Source={StaticResource lookup}}" />

                              <toolkit:DataGridComboBoxColumn Header="Editable Value" SelectedItemBinding="{Binding Path=Value}" ItemsSource="{Binding Source={StaticResource lookup}}">

                                     <toolkit:DataGridComboBoxColumn.EditingElementStyle>

                                            <Style TargetType="{x:Type ComboBox}">

                                                    <Setter Property="IsEditable" Value="True"></Setter>

                                                    <EventSetter Event="PreviewLostKeyboardFocus" Handler="ComboBox_PreviewLostKeyboardFocus"></EventSetter>

                                            </Style>

                                     </toolkit:DataGridComboBoxColumn.EditingElementStyle>

                              </toolkit:DataGridComboBoxColumn>

                        </toolkit:DataGrid.Columns>

                </toolkit:DataGrid>

                <TextBlock Margin="5">Read-Only Grid:</TextBlock>

                <toolkit:DataGrid ItemsSource="{Binding Source={StaticResource items}}"

                          IsReadOnly="True" />

        </StackPanel>

</Window>

Nov 5, 2008 at 7:30 AM
Hi,

Thanks for the suggestion, but that XAML was what I had tried previously.

To test your solution again I overwrote my existing XAML file with your XAML and then renamed the x:Class to use the Window1 code-behind implementation from my original example.  The behavior I observed at runtime was both correct and not correct, depending upon my actions, so I can see why you may have thought that it worked properly.

To repro the issue:

  1. Run the application with your XAML and my original code-behind.
  2. Left-click the Editable Value column once in the first row.
  3. Left-click the Editable Value column again, in the first row, so that it goes into edit mode.
  4. Type any value into the ComboBox.
  5. Left-click once anywhere else in the grid, such as on the third row, so that the focus leaves the first row.  The value you entered is committed and the Allowable Value column is automatically updated to reflect the change.  This is the correct behavior.
  6. Restart the application and perform the same steps again up to and including step #4.  (Alternatively, don't restart the application and just use the second row instead.)
  7. This time instead of left-clicking a different row to end the edit, press the Enter key.

The Allowable Value field is blank after pressing the Enter key.  In my original example you'll find that pressing Enter exhibits the same behavior as left-clicking to change the input focus, which is the expected behavior.

- Dave

Coordinator
Nov 5, 2008 at 6:01 PM
I see the problem now. That's becuase of how combobox handles focus when its editable and how enter key handler is implemented in DataGrid. But your implementation is not without problems too.
  • For instance we cannot use Text property of combobox in editing template of template column (Editable Value) along with SelectedValueBinding on ComboBox columns ("Allowed Value")
  • Also, since you are using a template column you would loose the entire built-in cancel functionality, which is quite essential in such editable combobox cells. (Try editing your template column cell and hit an escape)

Considering all these I am thinking another solution which would get rid of the dependency of focus change (which is kind of complicated in the context of editable combobox)....

       <toolkit:DataGrid CellEditEnding="DataGrid_CellEditEnding" ItemsSource="{Binding Source={StaticResource items}}" AutoGenerateColumns="False">

            <toolkit:DataGrid.Columns>

                <toolkit:DataGridTextColumn Header="Name" Binding="{Binding Name}"/>

                <toolkit:DataGridComboBoxColumn Header="Allowable Value" SelectedItemBinding="{Binding Value}" ItemsSource="{Binding Source={StaticResource lookup}}" />

                <toolkit:DataGridComboBoxColumn Header="Editable Value" x:Name="mycombo" SelectedItemBinding="{Binding Path=Value}" ItemsSource="{Binding Source={StaticResource lookup}}">

                    <toolkit:DataGridComboBoxColumn.EditingElementStyle>

                        <Style TargetType="{x:Type ComboBox}">

                            <Setter Property="IsEditable" Value="True"></Setter>

                        </Style>

                    </toolkit:DataGridComboBoxColumn.EditingElementStyle>

                </toolkit:DataGridComboBoxColumn>

            </toolkit:DataGrid.Columns>

        </toolkit:DataGrid>
 

        private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)

        {

                ComboBox box = e.EditingElement as ComboBox;

                if (e.EditAction == DataGridEditAction.Commit &&

                      e.Column == mycombo && // or some other way to identify that this is one of the columns to deal with

                      box != null)

                {

                       if (box.SelectedIndex < 0 && box.Text.Length > 0)

                       {

                             IList<string> items = (IList<string>)box.ItemsSource;

                             items.Add(box.Text); // real code should check if it not already exists

                             box.SelectedValue = box.Text;

                       }

                }

        }

This should work even if you commit the cell programatically using CommitEdit method of DataGrid.

Nov 6, 2008 at 11:34 AM
Hi,

Thanks for the solution, it seems to work well :)

- Dave