WPF DataGrid as ComboBox Dropdown

This article describes how to display the items of a WPF ComboBox inside a DataGrid when the ComboBox is clicked. It also shows how to make the behavior same as that of a normal ComboBox, like clicking the item on the DataGrid should close the ComboBox's popup.

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.

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