WPF Datagrid as ComboBox Dropdown Part 2

This presents a WPF custom control derived from ComboBox that shows a DataGrid to display the ComboBox items.

Introduction

Some time ago, I have written an article that discusses how to show ComboBox items using a DataGrid. The article shows how to change the control template of the ComboBox and replace the default control in the Popup with a DataGrid. It gets the job done but is not elegant, I must admit. If you want to do the same thing on other ComboBox controls, then you have to replicate the style and control template and change the columns of the DataGrid (since you probably don’t want to show the same columns over and over). This may result in long lines of XAML codes if there are a lot of ComboBox controls. It is pretty messy in my opinion.

I think that this issue can be resolved in different ways. It might be possible to create a style and use data triggers to select the columns that you want to display. However, I do not prefer this solution since it is not very intuitive. We have to look at the style definition that could be located at another file and check a long list of data triggers just to add or remove some DataGrid columns. We are left with creating either a user control or custom control. Creating a user control is easy since doing this is just like dragging controls to a Window. Yet, I am not really going to drag a lot of controls, only one ComboBox. Besides, I want the control to still be a ComboBox. If I created a user control, then it is very likely that I have to expose the properties of the ComboBox. That is just another level of indirection. Thus, my choice of implementation is to create a custom control.

Deriving from ComboBox

Before anything else, first create a project of type WPF Custom Control Library, so that things are automatically created for us. Our first goal is to create a custom control that derives from ComboBox and create a property where its value will be used as the content of the dropdown (or Popup control). We can create a dependency property of type UIElement for this.

public class ExtendedComboBox : ComboBox
{
    #region Dependency Properties

    public
static readonly DependencyProperty PopupContentProperty =
        DependencyProperty.
Register("PopupContent", typeof(UIElement), typeof(ExtendedComboBox));

    public UIElement PopupContent
    {
        get {
return (UIElement)GetValue(PopupContentProperty); }
        set {
SetValue(PopupContentProperty, value); }
    }

    #endregion

    #region Constructor

    static ExtendedComboBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
typeof(ExtendedComboBox), new FrameworkPropertyMetadata(typeof(ExtendedComboBox)));
    }

    #endregion
}


Listing 1. ComboBox with PopupContent Property

Now, we can reference the PopupContent dependency property in the default style of the ExtendedComboBox control found in Generic.xaml. I copied the XAML code in my previous article and changed some lines. I only copied the part that we are interested in, which is shown in the following code listing. The highlighted part shows the changes.

<Popup
    AllowsTransparency="true"
    IsOpen="{Binding Path=IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}"
    Placement="Bottom"
    PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"
    Margin="1"
    x:Name="PART_Popup"
    Grid.ColumnSpan="2">
    
<Microsoft_Windows_Themes:SystemDropShadowChrome
        
MaxHeight="{TemplateBinding MaxDropDownHeight}"
        
MinWidth="{Binding Path=ActualWidth, ElementName=MainGrid}"
        
x:Name="Shdw"
        
Color="Transparent">
        
<Border
            
x:Name="DropDownBorder"
            
Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
            
BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}"
            
BorderThickness="1">
            <ContentControl
                 
Content="{TemplateBinding PopupContent}"/>
      
</Border>
    
</Microsoft_Windows_Themes:SystemDropShadowChrome>
</Popup>

Listing 2. ExtendedComboBox Default Style

As you can see, the PopupContent’s value will be the content of the ContentControl that we added inside the Popup control. It’s up to you if you want to reference the PopupContent somewhere else. You could remove the SystemDropShadowChrome entirely and bind the Popup’s Child property to the PopupContent.

Adding a DataGrid

Let’s use the control in a demo application and check if it already satisfies our requirement. I have reused most of the codes in my previous article like the Customer and Customer classes, as well as the code-behind file for the main window, so I suggest you check it out.

<Window
    x:Class="DemoApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="clr-namespace:CustomControls;assembly=CustomControls"
    xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
    xmlns:local="clr-namespace:DemoApp"
    Title="MainWindow" Height="300" Width="300">
    <Grid>
        <Grid.Resources>
            <local:Customers x:Key="Customers"/>
        </Grid.Resources>
        <controls:ExtendedComboBox
            Width="250"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            ItemsSource="{StaticResource Customers}"
            SelectedItem="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=SelectedCustomer}"
            IsDropDownOpen="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=IsEditingCustomer}">
            <controls:ExtendedComboBox.PopupContent>
                 <toolkit:DataGrid
                     AutoGenerateColumns="False"
                     IsReadOnly="True"
                     ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type controls:ExtendedComboBox}}, Path=ItemsSource}"
                     SelectedItem="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type controls:ExtendedComboBox}}, Path=SelectedItem}">
                     <toolkit:DataGrid.Columns>
                         <toolkit:DataGridTextColumn
                              Header="Name"
                              Binding="{Binding Name}"/>
                         <toolkit:DataGridTextColumn
                             Header="Address"
                             Binding="{Binding Address}"/>
                         <toolkit:DataGridTextColumn
                              Header="Telephone No."
                            Binding="{Binding TelephoneNumber}"/>
                    </toolkit:DataGrid.Columns>
                </toolkit:DataGrid>
            </controls:ExtendedComboBox.PopupContent>
            <controls:ExtendedComboBox.ItemTemplate>
                <DataTemplate>
                     <StackPanel Orientation="Horizontal">
                         <TextBlock Text="{Binding Path=Name}" Margin="4,0"/>
                         <TextBlock Text="{Binding Path=Address}" Margin="4,0"/>
                         <TextBlock Text="{Binding Path=TelephoneNumber}" Margin="4,0"/>
                     </StackPanel>
                </DataTemplate>
            </controls:ExtendedComboBox.ItemTemplate>
        </controls:ExtendedComboBox>
    </Grid>
</Window>

Listing 3. Using the ExtendedComboBox Control

The ComboBox’s ItemsSource, SelectedItem and IsDropDownOpen properties are bounded to properties that are found in the MainWindow's code-behind file. The ItemsSource and SelectedItem properties of the DataGrid are then bounded to the matching properties of the ComboBox. The IsDropDownOpen property is used so that the Popup is closed when a different item in the DataGrid is selected. The following figure shows the demo application.



Figure 1. DataGrid in ComboBox Demo

Further Improvements

With the current control, we can have a DataGrid inside a ComboBox without much replication of codes. However, there are some things that we can still improve. First, we may want to remove the dependence on using IsDropDownOpen and IsEditingCustomer properties just to close the ComboBox’s Popup. I believe the user of the control does not need to be concerned with closing the Popup. The following shows two properties in the current code-behind file of the main window. These properties are used for closing the Popup. When the SelectedCustomer property value changes, it sets the IsEditingCustomer property to false, thus closing the Popup since the IsDropDownOpen property is bounded to the IsEditingProperty. This is really not the best solution.

public Customer SelectedCustomer
{
    get
    {
        return selectedCustomer;
    }
    set
    {
        if (object.ReferenceEquals(selectedCustomer, value) != true)
        {
            selectedCustomer =
value;
            if (PropertyChanged != null)
            {
                 PropertyChanged(this, new PropertyChangedEventArgs("SelectedCustomer"));
            }

            IsEditingCustomer =
false;
        }
    }
}

public bool IsEditingCustomer
{
    get
    {
        return isEditingCustomer;
    }
    set
    {
        if (isEditingCustomer.Equals(value) != true)
        {
            isEditingCustomer =
value;
            if (PropertyChanged != null)
            {
                 PropertyChanged(this, new PropertyChangedEventArgs("IsEditingCustomer"));
            }
        }
    }
}


Listing 4. Current Code for Closing the Popup

Second, we will always write the code to bind the ItemsSource and SelectedItem properties of the DataGrid to the matching properties of the ComboBox every time we use the control. It is nice if this could be done only once. To solve these problems, let’s create another control that will inherit from the ExtendedComboBox class.

public class ExtendedComboBoxDataGrid : ExtendedComboBox
{
    #region Private Fields

    private DataGrid dataGrid;

    #endregion

    #region Dependency Properties

    public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns",
typeof(ObservableCollection<DataGridColumn>), typeof(ExtendedComboBoxDataGrid));

    public ObservableCollection<DataGridColumn> Columns
    {
        get {
return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty); }
        set {
SetValue(ColumnsProperty, value); }
    }

    #endregion

    #region Constructors

    public ExtendedComboBoxDataGrid()
    {
        dataGrid =
new DataGrid();
        dataGrid.
IsReadOnly = true;
        dataGrid.
AutoGenerateColumns = false;

        RelativeSource relativeSource = new RelativeSource(RelativeSourceMode.FindAncestor,
typeof(ExtendedComboBoxDataGrid), 1);

        // Bind ItemsSource
       Binding itemsSourceBinding = new Binding("ItemsSource");
        itemsSourceBinding.
RelativeSource = relativeSource;
        dataGrid.
SetBinding(DataGrid.ItemsSourceProperty, itemsSourceBinding);

        // Bind SelectedItem
       Binding selectedItemBinding = new Binding("SelectedItem");
        selectedItemBinding.
RelativeSource = relativeSource;
        dataGrid.
SetBinding(DataGrid.SelectedItemProperty, selectedItemBinding);

        dataGrid.
SelectionChanged += DataGrid_SelectionChanged;

        PopupContent = dataGrid;

        Columns = new ObservableCollection<DataGridColumn>();
        Columns.CollectionChanged += Columns_CollectionChanged;
    }

    #endregion

    #region Private Methods

    private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
         IsDropDownOpen = false;
    }

    private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (DataGridColumn column in e.NewItems)
            {
                 dataGrid.
Columns.Add(column);
            }
        }

        if (e.OldItems != null)
        {
            foreach (DataGridColumn column in e.OldItems)
            {
                 dataGrid.
Columns.Remove(column);
            }
        }
    }

    #endregion
}


Listing 5. ComboBox with Default DataGrid Popup

The idea behind this control is to set the PopupContent to an instance of a DataGrid by default. This DataGrid will have its IsReadOnly property set to true and AutoGenerateColumns property to false. You could add dependency properties to the control if you want some properties of the DataGrid be exposed. Then, we bind the ItemsSource and SelectedItem properties of the DataGrid to the matching properties of the ComboBox, thus removing the need to write this for every control. Next, we subscribe to the SelectionChanged event of the DataGrid so that we can set the IsDropDownOpen property to false. After this, we can already remove the IsEditingCustomer property in our demo application. I have also added a Columns dependency property so that we can add columns to the DataGrid.

<controls:ExtendedComboBoxDataGrid
    Width="250"
    HorizontalAlignment="Center"
    VerticalAlignment="Center"
    ItemsSource="{StaticResource Customers}"
    SelectedItem="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=SelectedCustomer}">
    <controls:ExtendedComboBoxDataGrid.Columns>
        <toolkit:DataGridTextColumn
            Header="Name"
            Binding="{Binding Name}"/>
        <toolkit:DataGridTextColumn
            Header="Address"
            Binding="{Binding Address}"/>
        <toolkit:DataGridTextColumn
            Header="Telephone No."
            Binding="{Binding TelephoneNumber}"/>
    </controls:ExtendedComboBoxDataGrid.Columns>
    <controls:ExtendedComboBoxDataGrid.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                 <TextBlock Text="{Binding Path=Name}" Margin="4,0"/>
                 <TextBlock Text="{Binding Path=Address}" Margin="4,0"/>
                 <TextBlock Text="{Binding Path=TelephoneNumber}" Margin="4,0"/>
            </StackPanel>
        </DataTemplate>
    </controls:ExtendedComboBoxDataGrid.ItemTemplate>
</controls:ExtendedComboBoxDataGrid>

Listing 6. Using the ComboBox with Default DataGrid Popup

The new control does the same thing as the previous one but as you can see, the XAML code is cleaner.

Filtering DataGrid Items

Last thing that I want to discuss is that there are two control templates for the ExtendedComboBox control in Generic.xaml. The default control template is what we have been editing so far. The other one is used when the IsEditable property is set to true. This template shows an ItemsPresenter inside a ScrollViewer. As what we did in the default control template, we just need to replace the ScrollViewer with the ContentControl and bind the Content property to the PopupContent property of the ExtendedComboBox.



Figure 2. IsEditable Property Set to True

While we are at it, let’s add a simple auto-complete feature. When the text changes, we will filter the items of the DataGrid by setting the Filter property of the ItemCollection object (obtained from DataGrid.Items). To get the TextBox object that contains the text, we need to override the OnApplyTemplate method of the ExtendedComboBoxDataGrid control. Afterwards, we will subscribe to the TextChanged event and change the Filter property at the event handler.

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    TextBox editableTbx = GetTemplateChild("PART_EditableTextBox") as TextBox;
    if (editableTbx != null)
    {
        editableTbx.
TextChanged += EditableTbx_TextChanged;
    }
}

private void EditableTbx_TextChanged(object sender, TextChangedEventArgs e)
{
    TextBox editableTbx = (TextBox)sender;
    dataGrid.
Items.Filter = new Predicate<object>(
        o =>
       {
            return o.ToString().StartsWith(editableTbx.Text);
        }
    );
}


Listing 7. Filtering DataGrid Items

In the event handler, we compare the string representation of the DataGrid item with the text the user typed in the TextBox. In our demo application, we need to override the ToString method in the Customer class so that the Name is returned instead of the type.

public class Customer
{
    public string Name { get; set; }

    public string Address { get; set; }

    public string TelephoneNumber { get; set; }

    public override string ToString()
    {
        return Name;
    }
}


Listing 8. Override the ToString method

The following figure shows the demo application, this time with filtered items. Note that the search criterion is not case-sensitive.



Figure 3. DataGrid with Filtered Items

You can download the Visual Studio 2008 solution here. Note that you need to have WPF Toolkit installed to run the application.

By Michael Detras   Popularity  (13915 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog