In WPF we have lot of controls which we can customize to achieve our desired result. It can be done either through XAML or by creating custom library file also by combining both the way. Here I’m going to customize a ComboBox to accept multiple values only by using XAML file.
https://github.com/IncRevCorp/MultiSelectComboBox.XAMLWay
Idea behind it
“General idea of this to change the default ItemsPresenter control in the ComboBox to ListBox control which is having the option to accept multiple values”.
The initial code will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<Window x:Class="MultiSelectComboBox.XAMLWay.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" WindowState="Maximized"> <Grid Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="150"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Text="Multi-Select ComboBox in XAML way" Grid.ColumnSpan="2" FontSize="24" /> <TextBlock Text="Normal ComboBox" Grid.Row="1" /> <ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding Countries}" SelectedItem="{Binding SelectedCountries, Mode=TwoWay}" Width="300"></ComboBox> <TextBlock Text="Multi-Select ComboBox" Grid.Row="2" /> <ComboBox Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Cities}" SelectedItem="{Binding SelectedCity, Mode=TwoWay}" Width="300"></ComboBox> </Grid> </Window> |
Now we are going to start customizing the look like this:
It will modify the intial Xaml page like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<Window x:Class="MultiSelectComboBox.XAMLWay.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" WindowState="Maximized"> <Grid Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="150"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Text="Multi-Select ComboBox in XAML way" Grid.ColumnSpan="2" FontSize="24" /> <TextBlock Text="Normal ComboBox" Grid.Row="1" /> <ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding Countries}" SelectedItem="{Binding SelectedCountries, Mode=TwoWay}" Width="300"></ComboBox> <TextBlock Text="Multi-Select ComboBox" Grid.Row="2" /> <ComboBox Grid.Column="1" Grid.Row="2" ItemsSource="{Binding Cities}" SelectedItem="{Binding SelectedCity, Mode=TwoWay}" Template="{DynamicResource MultiSelectComboBoxStyle}" Width="300"></ComboBox> </Grid> </Window> |
And also in App.Xaml we will have code like
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
<ControlTemplate x:Key="MultiSelectComboBoxStyle" TargetType="{x:Type ComboBox}"> <Grid x:Name="MainGrid" SnapsToDevicePixels="True"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/> </Grid.ColumnDefinitions> <Popup x:Name="PART_Popup" AllowsTransparency="True" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom"> <Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}"> <Border x:Name="DropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"> <ScrollViewer x:Name="DropDownScrollViewer"> <Grid RenderOptions.ClearTypeHint="Enabled"> <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0"> <Rectangle x:Name="OpaqueRect" Fill="{Binding Background, ElementName=DropDownBorder}" Height="{Binding ActualHeight, ElementName=DropDownBorder}" Width="{Binding ActualWidth, ElementName=DropDownBorder}"/> </Canvas> <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </Grid> </ScrollViewer> </Border> </Themes:SystemDropShadowChrome> </Popup> <ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"> <ToggleButton.Style> <Style TargetType="{x:Type ToggleButton}"> <Setter Property="OverridesDefaultStyle" Value="True"/> <Setter Property="IsTabStop" Value="False"/> <Setter Property="Focusable" Value="False"/> <Setter Property="ClickMode" Value="Press"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" SnapsToDevicePixels="True"> <Grid HorizontalAlignment="Right" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}"> <Path x:Name="Arrow" Data="M0,0L3.5,4 7,0z" Fill="Black" HorizontalAlignment="Center" Margin="3,1,0,0" VerticalAlignment="Center"/> </Grid> </Themes:ButtonChrome> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="True"> <Setter Property="RenderPressed" TargetName="Chrome" Value="True"/> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Fill" TargetName="Arrow" Value="#FFAFAFAF"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </ToggleButton.Style> </ToggleButton> <ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="False" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </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> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsGrouping" Value="True"/> <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="False"/> </MultiTrigger.Conditions> <Setter Property="ScrollViewer.CanContentScroll" Value="False"/> </MultiTrigger> <Trigger Property="CanContentScroll" SourceName="DropDownScrollViewer" Value="False"> <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=DropDownScrollViewer}"/> <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=DropDownScrollViewer}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> |
In this template we need to modify the ItemsPresenter control inside the ControlTemplate to ListBox
1 |
<ListBox x:Name="lstBox" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" KeyboardNavigation.DirectionalNavigation="Contained" SelectionMode="Extended" ItemsSource="{TemplateBinding ItemsSource}"/> |
Here SelectionMode is the property which helps us to accept multiple values. It accepts three values Single, Multiple & Extended. As the name implies Single will accept single value, Multiple will accept multiple values & Extended will accept values based on ctrl & shift buttons as in Windows default functionality.
Now we will be able to accept multiple values & the next step is to display the selected values in the ComboBox selection. We can achieve this by modifying the ContentPresenter found in the template to ItemsControl.
1 2 3 4 5 6 |
<ItemsControl IsHitTestVisible="false" ItemsSource="{Binding Path=SelectedItems, ElementName=lstBox}" Margin="5 0" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel IsItemsHost="True" Orientation="Horizontal" VerticalAlignment="Center"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> |
ItemsControl.ItemsPanel is added to display the selected items in horizontal way.
But again here we need to separate the selected items by some means, so that we can easily distinguish it. For that purpose we can still customize the ItemsControl’s ItemTemplate like this to show and hide a “Comma” block base on the condition.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock x:Name="commaTextBlock" Text=", " Margin="0 5"/> <TextBlock Text="{Binding}" Margin="0 5"/> </StackPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}"> <Setter Property="Visibility" TargetName="commaTextBlock" Value="Collapsed"/> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ItemsControl.ItemTemplate> |
Now it looks perfect. We have done with the customization.Cheers!!!