WPF DataGrid as ComboBox Dropdown
I had a WPF application wherein I want to display a list of objects (ex. customers,
suppliers) in a ComboBox. What I want is to show many information at once, like
name, address, telephone number, etc. This can be achieved by changing the ItemTemplate
of the ComboBox. However, I want to display the properties of an item as columns
and the items as rows. Fortunately, I found out about the WPF DataGrid. I decided
to use the DataGrid to show the items when the ComboBox is clicked.
First things first. Since the WPF DataGrid is not officially released yet at this
time, so you need to download the latest WPFToolkit binaries from CodePlex -
http://wpf.codeplex.com/. The DataGrid is part of the WPFToolkit library. The
library has other useful controls as well. After installing the library you can already see the added controls in the Visual
Studio toolbox.
The example I created is very simple. It contains a Window that has one ComboBox.
The ItemsSource property is bounded to an ObservableCollection<Customer>
object. The Customer class is shown in the following listing.
public class Customer
{
public string Name { get; set; }
public string Address { get; set; }
public string TelephoneNumber { get; set; }
}
I could show all the information in the ComboBox without using a DataGrid by specifying
the ItemTemplate, which is shown in the following code.
<Window.Resources>
<local:Customers x:Key="Customers"/>
</Window.Resources>
<Grid>
<ComboBox
Margin="4"
Height="23"
Width="250"
ItemsSource="{StaticResource Customers}">
<ComboBox.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>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
This produces the following output using some data I supplied.

Now, the only thing left to do is to use a DataGrid as the dropdown of the ComboBox.
To do this, we have to change the default ComboBox ControlTemplate. Getting the
default template is easy by using Expression Blend. There is another way but
it won’t be discussed in this article. Open the project in Expression Blend.
Right-click on the ComboBox and choose Edit Control Parts (Template) then Edit
a Copy... as shown in the following screenshot.

This will prompt you with a dialog asking about where to put the new style resource.
Simply choose the default values. After clicking on OK, this generates a long
listing in the XAML file, located in the Window’s resources region. I only copied
the code that we are interested in.
<ControlTemplate
TargetType="{x:Type ComboBox}">
<Grid
SnapsToDevicePixels="true"
x:Name="MainGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*"/>
<ColumnDefinition
MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}"
Width="0"/>
</Grid.ColumnDefinitions>
<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">
<ScrollViewer
CanContentScroll="true">
<ItemsPresenter
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
KeyboardNavigation.DirectionalNavigation="Contained"/>
</ScrollViewer>
</Border>
</Microsoft_Windows_Themes:SystemDropShadowChrome>
</Popup>
<ToggleButton
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
Style="{StaticResource ComboBoxReadonlyToggleButton}"
Grid.ColumnSpan="2"
IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<ContentPresenter
IsHitTestVisible="false"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasDropShadow" SourceName="PART_Popup" Value="true">
<Setter Property="Margin" TargetName="Shdw" Value="0,0,5,5"/>
<Setter Property="Color" TargetName="Shdw" Value="#71000000"/>
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter Property="Height" TargetName="DropDownBorder" Value="95"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
<Setter Property="Background" Value="#FFF4F4F4"/>
</Trigger>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
This is the ControlTemplate obtained from the ComboBox’s default style. In my example,
the style is identified as ComboBoxStyle1. What we need to edit is the Popup part of the ControlTemplate.
The items are presented using an ItemsPresenter inside a ScrollViewer object.
Let’s replace this with a DataGrid. To use the DataGrid, we have to add a reference
to the WPF Toolkit in our project. After this, add the following XML namespace
in the XAML file. You can use a different name if you want.
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
Now, replace the ScrollViewer with our DataGrid. I set the AutoGenerateColumns property
to False because I want to set the Header property of each column. The ItemsSource
is bound to the ItemsSource property of the ComboBox using TemplateBinding.
<toolkit:DataGrid
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
ItemsSource="{TemplateBinding ItemsSource}"
AutoGenerateColumns="False">
<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>
Using the same data earlier, the window looks like this.

So far, so good. However, there are things that we still need to do. As much as possible,
we want our control to behave much like a normal ComboBox. First, the DataGrid
values can be edited and users can add or delete rows. We certainly do not like this behavior so let’s set the IsReadOnly property of the
DataGrid to True. Second, we can select an item in the DataGrid but it is not
set as the selected item of the ComboBox. To solve this, bind the SelectedItem
property of the DataGrid to the SelectedItem property of the ComboBox, as shown
in the following code.
SelectedItem="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedItem}"
I thought I could use TemplateBinding here, but it does not work. I think TemplateBinding
is a bit specialized and that the binding direction is only one-way, which makes
sense since the templates do not normally set the value of a property of their
templated control. This also improves performance. Anyway, we can solve our problem
by using RelativeSource.
Lastly, the popup remains open even after selecting an item. No surprise here. To
close the popup, we need to be able to set the IsDropDown property of the ComboBox.
To do this, we could bind the IsDropDown property to a property defined in the Window.
The following code example shows the binding.
IsDropDownOpen="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=IsEditingCustomer}
We could set the value of the IsEditingCustomer property to control when to close
the ComboBox popup. The IsEditingCustomer property is defined in the Window containing
the ComboBox. So the question is when to set the value of this property. Well,
it should be after the SelectedItem of the ComboBox has been changed. So let’s
bind the SelectedItem to another property.
SelectedItem="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=SelectedCustomer}"
When the SeletedCustomer changes, the IsEditingCustomers property is set to false.
The following listing shows the Window class.
public partial class Window1 : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Customer selectedCustomer;
private bool isEditingCustomer;
public Window1()
{
InitializeComponent();
}
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"));
}
}
}
}
}
I implemented the INotifyPropertyChanged interface so that the UI is notified that
the source properties are changed. Without this, we won’t be able to close the
ComboBox popup. Notice that in the SelectedCustomer property’s setter, the IsEditingCustomer
is set to false. There are other ways aside from using data binding in closing
the ComboBox popup though.
That’s about it. That was a very simple implementation of a ComboBox that shows its
items inside a WPF DataGrid. It can still be improved. The implementation changes
the style of the ComboBox. If there are many ComboBox controls, then a style
should be created for each one of them since they won’t have the same type of
items. You could set the ControlTemplate directly instead to minimize defining
many styles. Better yet, you can create a UserControl so that it can be reused
easily.
Another thing that this implementation lacks is custom paging and sorting. For me,
I don’t want to load all the database items in memory at once. It might affect
the performance of the application or the system. So I would want to implement
paging. When you have implemented paging already, then comes sorting. The default
sorting mechanism of the DataGrid only applies to the current items. So I have
to implement custom sorting so that all items are considered.
Here is the link to the example I created: WPFDataGridAsComooBoxDropdown.zip. Take note that you still need to download WPF Tookit from Codeplex to run the application.