Sam Sch
Sam Sch

Reputation: 672

Customize ComboBox.ItemsPanel

I want to customize panel that drops down from ComboBox.

For example, for adding search functionality or something else.

I know about EssentialObjects, and this is exactly what I need, but I want to know how to do it myself.

Little draft:

<ComboBox ...>
    <ComboBox.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- This is how i can customize container for items. But it's not the way -->
            <!-- <UniformGrid Columns="2" /> -->

            <!-- But i need something like this -->
            <Grid>
                <!-- Whatever i want -->
                <TextBox ... />
                <Button ... />
                <Button ... />

                <!-- Container for items -->
                <UniformGrid Columns="2"/>
            </Grid>

        </ItemsPanelTemplate>
    </ComboBox.ItemsPanel>
</ComboBox>

Is it possible to tune standard ComboBox, or will I have to create my own control?

Upvotes: 1

Views: 1316

Answers (1)

Il Vic
Il Vic

Reputation: 5666

My suggestion is to create your own ComboBox which extends the standard one. Let's see how. First of all the control class:

public class CustomComboBox : ComboBox
{
    public static readonly DependencyProperty HeaderTemplateProperty =
        DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(CustomComboBox), new PropertyMetadata(null));

    public static readonly DependencyProperty FooterTemplateProperty =
        DependencyProperty.Register("FooterTemplate", typeof(DataTemplate), typeof(CustomComboBox), new PropertyMetadata(null));

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

    public DataTemplate HeaderTemplate
    {
        get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
        set { SetValue(HeaderTemplateProperty, value); }
    }

    public DataTemplate FooterTemplate
    {
        get { return (DataTemplate)GetValue(FooterTemplateProperty); }
        set { SetValue(FooterTemplateProperty, value); }
    }
}

We are just adding two dependecy property to manage header and footer. Now using ILSpy you can retrieve the default ComboBox style. We need to use our control template:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:cbd="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Classic"
                    xmlns:local="clr-namespace:WpfApp1">

    <Geometry x:Key="Û">M 0 0 L 3.5 4 L 7 0 Z</Geometry>

    <Style x:Key="Ü" TargetType="{x:Type ToggleButton}">
        <Setter Property="FrameworkElement.MinWidth" Value="0" />
        <Setter Property="FrameworkElement.MinHeight" Value="0" />
        <Setter Property="FrameworkElement.Width" Value="Auto" />
        <Setter Property="FrameworkElement.Height" Value="Auto" />
        <Setter Property="Control.Background" Value="#00FFFFFF" />
        <Setter Property="Control.BorderBrush" Value="{x:Static cbd:ClassicBorderDecorator.ClassicBorderBrush}" />
        <Setter Property="Control.BorderThickness" Value="2" />
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ToggleButton}">
                    <DockPanel Background="{TemplateBinding Control.Background}" LastChildFill="False" SnapsToDevicePixels="True">
                        <cbd:ClassicBorderDecorator x:Name="Border" BorderStyle="AltRaised" DockPanel.Dock="Right" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" BorderThickness="{TemplateBinding Control.BorderThickness}" BorderBrush="{TemplateBinding Control.BorderBrush}">
                            <Path Fill="{TemplateBinding Control.Foreground}" HorizontalAlignment="Center" VerticalAlignment="Center" Data="{StaticResource Û}" />
                        </cbd:ClassicBorderDecorator>
                    </DockPanel>
                    <ControlTemplate.Triggers>
                        <Trigger Property="ToggleButton.IsChecked" Value="true">
                            <Setter TargetName="Border" Property="cbd:ClassicBorderDecorator.BorderStyle" Value="AltPressed" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="UIElement.IsEnabled" Value="False">
                <Setter Property="Control.Foreground" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" />
            </Trigger>
        </Style.Triggers>
    </Style>

    <Style BasedOn="{StaticResource {x:Type ComboBox}}" TargetType="{x:Type local:CustomComboBox}">
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ComboBox}">
                    <Border Background="{TemplateBinding Control.Background}" BorderThickness="{TemplateBinding Control.BorderThickness}" BorderBrush="{TemplateBinding Control.BorderBrush}" SnapsToDevicePixels="True">
                        <Grid>
                            <cbd:ClassicBorderDecorator x:Name="Border" Background="{TemplateBinding Control.Background}" BorderBrush="{x:Static cbd:ClassicBorderDecorator.ClassicBorderBrush}" BorderThickness="2" BorderStyle="Sunken">
                                <Popup Name="PART_Popup" AllowsTransparency="True" Placement="Bottom" IsOpen="{TemplateBinding ComboBox.IsDropDownOpen}" Focusable="False" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}">
                                    <cbd:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding ComboBox.MaxDropDownHeight}" MinWidth="{Binding ElementName=Border, Path=ActualWidth}">
                                        <Border Name="DropDownBorder" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" BorderThickness="1" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}">
                                            <ScrollViewer Name="DropDownScrollViewer">
                                                <Grid RenderOptions.ClearTypeHint="Enabled">
                                                    <Canvas Height="0" Width="0" HorizontalAlignment="Left" VerticalAlignment="Top">
                                                        <Rectangle Name="OpaqueRect" Height="{Binding ElementName=DropDownBorder, Path=ActualHeight}" Width="{Binding ElementName=DropDownBorder, Path=ActualWidth}" Fill="{Binding ElementName=DropDownBorder, Path=Background}" />
                                                    </Canvas>
                                                    <StackPanel>
                                                        <ContentPresenter Margin="1,1,1,1" ContentTemplate="{TemplateBinding local:CustomComboBox.HeaderTemplate}" />
                                                        <ItemsPresenter Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                                                        <ContentPresenter Margin="1,1,1,1" ContentTemplate="{TemplateBinding local:CustomComboBox.FooterTemplate}" />
                                                    </StackPanel>
                                                </Grid>
                                            </ScrollViewer>
                                        </Border>
                                    </cbd:SystemDropShadowChrome>
                                </Popup>
                            </cbd:ClassicBorderDecorator>
                            <DockPanel Margin="2">
                                <FrameworkElement DockPanel.Dock="Right" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" />
                                <Border Name="SelectedItemBorder" Margin="{TemplateBinding Control.Padding}">
                                    <ContentPresenter Margin="1,1,1,1" Content="{TemplateBinding ComboBox.SelectionBoxItem}" ContentTemplate="{TemplateBinding ComboBox.SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemsControl.ItemTemplateSelector}" ContentStringFormat="{TemplateBinding ComboBox.SelectionBoxItemStringFormat}" VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                                </Border>
                            </DockPanel>
                            <ToggleButton Margin="2" MinWidth="0" MinHeight="0" Width="Auto" Focusable="False" Style="{StaticResource Ü}" ClickMode="Press" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
                        </Grid>
                    </Border>
                    <ControlTemplate.Triggers>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="ComboBox.IsSelectionBoxHighlighted" Value="True" />
                                <Condition Property="ComboBox.IsDropDownOpen" Value="False" />
                            </MultiTrigger.Conditions>
                            <Setter Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" Property="Control.Foreground" />
                        </MultiTrigger>
                        <Trigger Property="ComboBox.IsSelectionBoxHighlighted" Value="True">
                            <Setter TargetName="SelectedItemBorder" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" Property="Border.Background" />
                        </Trigger>
                        <Trigger Property="ItemsControl.HasItems" Value="False">
                            <Setter TargetName="DropDownBorder" Property="FrameworkElement.MinHeight" Value="95" />
                        </Trigger>
                        <Trigger Property="UIElement.IsEnabled" Value="False">
                            <Setter Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" Property="Control.Foreground" />
                            <Setter Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Property="Control.Background" />
                        </Trigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="ItemsControl.IsGrouping" Value="True" />
                                <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="False" />
                            </MultiTrigger.Conditions>
                            <Setter Property="ScrollViewer.CanContentScroll" Value="False" />
                        </MultiTrigger>
                        <Trigger SourceName="PART_Popup" Property="Popup.HasDropShadow" Value="True">
                            <Setter TargetName="Shdw" Property="FrameworkElement.Margin" Value="0,0,5,5" />
                            <Setter TargetName="Shdw" Property="cbd:SystemDropShadowChrome.Color" Value="#71000000" />
                        </Trigger>
                        <Trigger SourceName="DropDownScrollViewer" Property="ScrollViewer.CanContentScroll" Value="False">
                            <Setter TargetName="OpaqueRect" Value="{Binding ElementName=DropDownScrollViewer, Path=VerticalOffset}" Property="Canvas.Top" />
                            <Setter TargetName="OpaqueRect" Value="{Binding ElementName=DropDownScrollViewer, Path=HorizontalOffset}" Property="Canvas.Left" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

I chose the classic theme, so in this case a reference to the PresentationFramework.classic is needed to be added to the project.

The trick is that I added two ContentPresenters (one before and one after) the ItemPresenter called ItemsPresenter. They will show our custom DataTemplates. This style should be placed in the Themes\generic.xaml file (set its build action to Page).

Now we can use the CustomComboBox:

<StackPanel>
    <local:CustomComboBox>
        <local:CustomComboBox.HeaderTemplate>
            <DataTemplate>
                <TextBlock Text="HEADER" />
            </DataTemplate>
        </local:CustomComboBox.HeaderTemplate>
        <local:CustomComboBox.FooterTemplate>
            <DataTemplate>
                <TextBlock Text="FOOTER" />
            </DataTemplate>
        </local:CustomComboBox.FooterTemplate>

        <ComboBoxItem>One</ComboBoxItem>
        <ComboBoxItem>Two</ComboBoxItem>
        <ComboBoxItem>Three</ComboBoxItem>
    </local:CustomComboBox>
</StackPanel>

I hope it can help you.

Upvotes: 3

Related Questions