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.