Happy new year to everyone!
I am sure many of us had a time when we require to create grouped items in the combo box - same way I required. I needed to create the combo box with look like the address bar of Internet explorer - where there are three groups of links:
Let's create such combo box step by step - which can be named as Custom Combo box, Combo box with grouped items or Combo box with Internet Explorer Address bar style - which will look like:
I have used Visual Studio 2010 for this example. Create a new WPF Application in Visual Studio and give it any suitable name. I have used 'ComboBoxWithGroupedItems'.
It will add Xaml page with default Grid added to your window. Simply add new combo box to this window:
Now, its time to create the informative class for our URL information. Add new class to your project - named - 'URLDetails' with following members:
Link property will hold Link to be opened and GroupName property will hold the name of the group under which the item should be displayed. Let's define one property of type ObservableCollection in the .cs file of our Xaml as shown below. To use the ObservableCollection, you will have to add 'using System.Collections.ObjectModel;' at the top of your xaml.cs file.
Now, we have property ready to store the details or URLs. We will now fill dummy data to this property. Let's create private method to do this for us, and call this method from the constructor of xaml.cs file as shown below:
OK, now we have data ready to bind with combo box. But before doing this, we want to have some grouping applied there. So, lets first define the CollectionViewSource with Grouping in Xaml file as shown below. Don't forget to refer to xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" as shown in below code.
Now, lets add template for group header and group item description in Window.Resources only. Here, Link template says that value of Link property should be displayed as item.
Now, we need to define style for combo box group item, to tell compiler to add Expander for each group item - so grouped items can be expanded and collapsed. Add below style in Window.Resources:
Hurray! We have got something similar what we want to achieve. But observe that,
Also, for calculating the minimum number of items to be displayed while the group is collapsed, we need to add converter for calculation of height:
I am sure many of us had a time when we require to create grouped items in the combo box - same way I required. I needed to create the combo box with look like the address bar of Internet explorer - where there are three groups of links:
- Group without any title showing the just visited URL
- History group showing URL used in past
- Favourite group showing the favourite URLs
Let's create such combo box step by step - which can be named as Custom Combo box, Combo box with grouped items or Combo box with Internet Explorer Address bar style - which will look like:
I have used Visual Studio 2010 for this example. Create a new WPF Application in Visual Studio and give it any suitable name. I have used 'ComboBoxWithGroupedItems'.
It will add Xaml page with default Grid added to your window. Simply add new combo box to this window:
<stackpanel> <combobox height="30" width="300"> </combobox> </stackpanel>
Now, its time to create the informative class for our URL information. Add new class to your project - named - 'URLDetails' with following members:
public class URLDetails { public URLDetails() { } public string Link { get; set; } public string GroupName { get; set; } }
Link property will hold Link to be opened and GroupName property will hold the name of the group under which the item should be displayed. Let's define one property of type ObservableCollection in the .cs file of our Xaml as shown below. To use the ObservableCollection, you will have to add 'using System.Collections.ObjectModel;' at the top of your xaml.cs file.
private ObservableCollection<URLDetails> _URLs; public ObservableCollection<URLDetails> URLs { get { return _URLs; } set { _URLs = value; } }
Now, we have property ready to store the details or URLs. We will now fill dummy data to this property. Let's create private method to do this for us, and call this method from the constructor of xaml.cs file as shown below:
const string MRUGroupName = "Most Recently Used"; const string HistoryGroupName = "History"; const string FavouritesGroupName = "Favourites"; public MainWindow() { InitializeComponent(); FillDummyData(); } private void FillDummyData() { _URLs = new ObservableCollection<URLDetails>(); // Add MRU link _URLs.Add(new URLDetails { GroupName = MRUGroupName, Link = "www.msdn.com" }); // Add History links _URLs.Add(new URLDetails { GroupName = HistoryGroupName, Link = "www.google.co.in" }); _URLs.Add(new URLDetails { GroupName = HistoryGroupName, Link = "www.yahoo.com" } ); _URLs.Add(new URLDetails { GroupName = HistoryGroupName, Link = "www.msn.com" } ); _URLs.Add(new URLDetails { GroupName = HistoryGroupName, Link = "https://mail.google.com" } ); _URLs.Add(new URLDetails { GroupName = HistoryGroupName, Link = "www.msdn.com" } ); _URLs.Add(new URLDetails { GroupName = HistoryGroupName, Link = "www.wikipedia.com" } ); // Add Favorite links _URLs.Add(new URLDetails { GroupName = FavouritesGroupName, Link = "www.mymail.com" } ); _URLs.Add(new URLDetails { GroupName = FavouritesGroupName, Link= "www.codeproject.com" } ); _URLs.Add(new URLDetails { GroupName = FavouritesGroupName, Link = "www.songs.in" } ); }
OK, now we have data ready to bind with combo box. But before doing this, we want to have some grouping applied there. So, lets first define the CollectionViewSource with Grouping in Xaml file as shown below. Don't forget to refer to xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" as shown in below code.
<window height="400" title="My Grouped Combo" width="350" x:class="ComboBoxWithGroupedItems.MainWindow" xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <window.resources> <collectionviewsource source="{Binding URLs}" x:key="GroupedData"> <collectionviewsource.groupdescriptions> <propertygroupdescription propertyname="GroupName"> </propertygroupdescription> </CollectionViewSource.GroupDescriptions> </window.resources> </window>
Now, lets add template for group header and group item description in Window.Resources only. Here, Link template says that value of Link property should be displayed as item.
<DataTemplate x:Key="GroupHeader"> <TextBlock Text="{Binding Name}" Margin="10,0,0,0" Foreground="#989791" /> <:/DataTemplate> <DataTemplate x:Key="URLTemplate"> <TextBlock Text="{Binding Link}" Margin="10,0,0,0"/> </DataTemplate>
Now, we need to define style for combo box group item, to tell compiler to add Expander for each group item - so grouped items can be expanded and collapsed. Add below style in Window.Resources:
OK, now we are ready to bind this data source with combo-box:
<StackPanel> <ComboBox Width="300" Grid.Row="0" Height="30" ItemsSource="{Binding Source={StaticResource GroupedData}}" ItemTemplate="{StaticResource URLTemplate}"> <ComboBox.GroupStyle> <GroupStyle ContainerStyle="{StaticResource containerStyle}" HeaderTemplate="{StaticResource GroupHeader}"> </GroupStyle> </ComboBox.GroupStyle> </ComboBox> </StackPanel>And, here it is - how it will look when you run the application:
Hurray! We have got something similar what we want to achieve. But observe that,
- The expander button is still having default arrow style, its on the left side of header text (which we want on right most side of group header),
- There is no line present in the header text, and
- When the group is collapsed, no item is being shown (which we want to show some configured item at least when its collapsed).
public int NumberOfItems { get { return 3; } }
public class HeightConverter : IMultiValueConverter { /// There will be two values here - first one will be the collection group bound - one call per group in combo box. /// The second will be the desired items to be displayed when the group is not expanded. /// /// /// ///public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { double returnValue; // Find out the total number of items in the current group i.e. 4 items in Most Recently Used group. int itemCount = ((System.Windows.Data.CollectionViewGroup)(((System.Windows.FrameworkElement)(values[0])).DataContext)).ItemCount; // This is the number of items to be displayed when the group is not expanded. int configuredItems = (int)values[1]; // Now, if there are less items available in particular group than configured, then we will display all the items available in group // else we will display the configured number of items only. This means, if configured item number is 5 but there are only 3 items in // any group, then we will consider 3 items and will return height to display exact 3 items only. if (configuredItems >= itemCount) { // SystemParameters.SmallIconHeight - return the required height for one row - this is required because // if you configured it hard coded, then when user will change the resolution, the number of items displayed will not be // what you wish i.e. if resolution is incresed, then instead of 3 - it will display 4 items and vice versa. returnValue = itemCount * SystemParameters.SmallIconHeight; } else { returnValue = configuredItems * SystemParameters.SmallIconHeight; } // This 2 is added just to ensure some space is left at the end of combo box. return returnValue + 2; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
<Window x:Class="ComboBoxWithGroupedItems.MainWindow" xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:local="clr-namespace:ComboBoxWithGroupedItems" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="My Grouped Combo" Height="400" Width="350" x:Name="MyWindow"> <Window.Resources> <!-- Fill Brushes, which are required by the control template of combo box - so we have copied them. --> <local:HeightConverter x:Key="MyHeightConverter" /> <LinearGradientBrush x:Key="NormalBrush" StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#FFF" Offset="0.0"/> <GradientStop Color="#CCC" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="HorizontalNormalBrush" StartPoint="0,0" EndPoint="1,0"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#FFF" Offset="0.0"/> <GradientStop Color="#CCC" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="LightBrush" StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#FFF" Offset="0.0"/> <GradientStop Color="#EEE" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="HorizontalLightBrush" StartPoint="0,0" EndPoint="1,0"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#FFF" Offset="0.0"/> <GradientStop Color="#EEE" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="DarkBrush" StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#FFF" Offset="0.0"/> <GradientStop Color="#AAA" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="PressedBrush" StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#BBB" Offset="0.0"/> <GradientStop Color="#EEE" Offset="0.1"/> <GradientStop Color="#EEE" Offset="0.9"/> <GradientStop Color="#FFF" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" /> <SolidColorBrush x:Key="DisabledBackgroundBrush" Color="#EEE" /> <SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF" /> <SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#DDD" /> <!-- Border Brushes --> <LinearGradientBrush x:Key="NormalBorderBrush" StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#CCC" Offset="0.0"/> <GradientStop Color="#444" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="HorizontalNormalBorderBrush" StartPoint="0,0" EndPoint="1,0"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#CCC" Offset="0.0"/> <GradientStop Color="#444" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="DefaultedBorderBrush" StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#777" Offset="0.0"/> <GradientStop Color="#000" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush x:Key="PressedBorderBrush" StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#444" Offset="0.0"/> <GradientStop Color="#888" Offset="1.0"/> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <SolidColorBrush x:Key="DisabledBorderBrush" Color="#AAA" /> <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" /> <SolidColorBrush x:Key="LightBorderBrush" Color="#AAA" /> <!-- Miscellaneous Brushes --> <SolidColorBrush x:Key="GlyphBrush" Color="#444" /> <SolidColorBrush x:Key="LightColorBrush" Color="#DDD" /> <!--This is the default toggle button style - we have not played it with it, just leave it as it is. If you wish, then you can change the expanded and collapsed image of this button too. --> <ControlTemplate x:Key="ExpanderToggleButton" TargetType="{x:Type ToggleButton}"> <Border x:Name="Border" CornerRadius="2,0,0,0" Background="Transparent" BorderBrush="{StaticResource NormalBorderBrush}" BorderThickness="0,0,0,0"> <Path x:Name="Arrow" Fill="{StaticResource GlyphBrush}" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M 0 0 L 4 4 L 8 0 Z"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="true"> <Setter TargetName="Border" Property="Background" Value="{StaticResource DarkBrush}" /> </Trigger> <Trigger Property="IsPressed" Value="true"> <Setter TargetName="Border" Property="Background" Value="{StaticResource PressedBrush}" /> </Trigger> <Trigger Property="IsChecked" Value="true"> <Setter TargetName="Arrow" Property="Data" Value="M 0 4 L 4 0 L 8 4 Z" /> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackgroundBrush}" /> <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DisabledBorderBrush}" /> <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/> <Setter TargetName="Arrow" Property="Fill" Value="{StaticResource DisabledForegroundBrush}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> <Style TargetType="{x:Type Expander}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Expander}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition x:Name="ContentRow"/> </Grid.RowDefinitions> <Border x:Name="Border" Grid.Row="0" Background="{StaticResource WindowBackgroundBrush}" BorderBrush="{StaticResource NormalBorderBrush}" BorderThickness="0,0,0,0" CornerRadius="2,2,0,0" > <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="20" /> </Grid.ColumnDefinitions> <!--Here, we have played a bit, to move the toggle button to expand and collapse the group to the right corner of the group header, so we placed this button in column 1 instead of column 0.--> <ToggleButton IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" OverridesDefaultStyle="True" Grid.Column="1" Template="{StaticResource ExpanderToggleButton}" Background="{StaticResource NormalBrush}" /> <Grid Grid.Column="0"> <!--We have modified this grid to have Group name i.e. ContentPresenter and than a line upto the expand/collapse button.--> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--This content presenter will have value of 'Header' which in our case is group name.--> <ContentPresenter Margin="2" ContentSource="Header" RecognizesAccessKey="True" Grid.Column="0" /> <Line X1="0" Y1="0" X2="1" Y2="0" Stretch="Fill" Stroke="DimGray" Grid.Column="1" /> </Grid> </Grid> </Border> <Border x:Name="Content" Grid.Row="1" Background="{StaticResource WindowBackgroundBrush}" BorderBrush="{StaticResource SolidBorderBrush}" BorderThickness="0,0,0,0" CornerRadius="0,0,2,2" > <!--This is border, in which the entire content is placed, and we need to play with height of this border in order to dispaly only desired number of items when group is collapsed. We have defined height converter class in our project. We are passing the Content and NumberOfItems to this converter using multibinding.--> <Border.Height> <MultiBinding Converter="{StaticResource MyHeightConverter}"> <Binding ElementName="ComboExpander" Path="Content" /> <Binding ElementName="MyWindow" Path="NumberOfItems"/> </MultiBinding> </Border.Height> <ContentPresenter Margin="10,2,0,0" /> </Border> </Grid> <!--This is regular expand button and we have left it as it was.--> <ControlTemplate.Triggers> <Trigger Property="IsExpanded" Value="True"> <Setter TargetName="Content" Property="Height" Value="{Binding DesiredHeight, ElementName=Content}"/> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackgroundBrush}" /> <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DisabledBorderBrush}" /> <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <CollectionViewSource x:Key="GroupedData" Source="{Binding URLs}"> <!--Here we are specifying on which field of collection item should be grouped--> <CollectionViewSource.GroupDescriptions> <PropertyGroupDescription PropertyName="GroupName"/> </CollectionViewSource.GroupDescriptions> </CollectionViewSource> <!--Here we tell that the Header of each group should be displaying the group name--> <DataTemplate x:Key="GroupHeader"> <TextBlock Text="{Binding Name}" Margin="10,0,0,0" Foreground="#989791" /> </DataTemplate> <!--Here, we tell that the URL's description should be displayed as item text.--> <DataTemplate x:Key="URLTemplate"> <TextBlock Text="{Binding Link}" Margin="10,0,0,0"/> </DataTemplate> <!-- Container Style for 1st level of grouping --> <Style x:Key="containerStyle" TargetType="{x:Type GroupItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type GroupItem}"> <!--Here, we tell that each group of item will be placed under Expander control, and this expander control will by default will have style we defined in above code.--> <Expander IsExpanded="False" x:Name="ComboExpander" Header="{TemplateBinding Content}" HeaderTemplate="{TemplateBinding ContentTemplate}"> <ItemsPresenter /> </Expander> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <StackPanel> <ComboBox Width="300" Grid.Row="0" Height="30" ItemsSource="{Binding Source={StaticResource GroupedData}}" ItemTemplate="{StaticResource URLTemplate}"> <ComboBox.GroupStyle> <GroupStyle ContainerStyle="{StaticResource containerStyle}" HeaderTemplate="{StaticResource GroupHeader}"> </GroupStyle> </ComboBox.GroupStyle> </ComboBox> </StackPanel> </Window>And here we are done. It would be showing the desired look and behaviour. Here is the sample solution uploaded: Download
Enjoy!
This comment has been removed by a blog administrator.
ReplyDeleteHi,
ReplyDeleteI liked the article and it is very helpful.
I like to know whether a particular group can be made expanded by default say "MostRecentlyUsed" and other groups collapsed.
Thanks in advance.
Does not work anymore... SourceCode is not available anymore.
ReplyDeleteSourceCode is not available anymore. Any one has the src?
ReplyDeleteCool and I have a super offer you: Who Repairs House Siding home remodeling contractors
ReplyDelete