Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Multi Select ComboBox in WPF

0.00/5 (No votes)
23 Jul 2013 1  
How to create a multi select combobox user control step by step.

Introduction

Recently in our project, we wanted to allow the user to select multiple values in a list. But the list should be populated inside a grid row. So we didn’t want to use a listbox and also we were not interested in third party tools. Instead of that, we wanted to use a multiselect combobox. When I browsed through various blogs, forums, etc., I got a few good codes, but none of them worked with MVVM pattern. In those articles, most of the datasource bindings were done at code behind. So I have changes to those existing code to support MVVM. In this article, I am going to explain how we can create a multi select combobox user control step by step. This article will also help people who have recently started learning WPF, since I have explained how we can create styles and dependency properties.

Using the Code

Create a new WPF User Control library. Rename the User Control to MultiSelectComboBox.

In order to create a multi-select combobox, we have to analyze what is required to construct such a control. We need a combobox and for each item in the combobox dropdown, we need to add a checkbox. Since we are going to write our custom data template for combobox items, we can’t just directly use a combobox. What we can do is design the template of the combo box well apart from the defining item template.

Our combobox should look like the one shown below:

In order to achieve this, as I mentioned in the above picture, we need a toggle button to determine, open/close the dropdown, and also to display the selected values. We need a popup control inside which we will be displaying all our items with a checkbox. All these together form our custom multi-select combobox control.

I am going to split the article into three parts now:

  1. Defining the styles and creating the XAML file
  2. Adding dependency properties and other objects in the code-behind of the user control
  3. Use this multiselect combobox DLL in some other XAML application

Defining the Styles and Templates

Remove the grid tags and add a combobox in the user control.

<ComboBox
        x:Name="MultiSelectCombo"  
        SnapsToDevicePixels="True"
        OverridesDefaultStyle="True"
        ScrollViewer.HorizontalScrollBarVisibility="Auto"
        ScrollViewer.VerticalScrollBarVisibility="Auto"
        ScrollViewer.CanContentScroll="True"
        IsSynchronizedWithCurrentItem="True"
             >

Add an item template for the above combobox. The item template should be a checkbox. Bind the content property of your checkbox to some property Title. Remember we didn’t start with the code-behind. We have to set this property in the code-behind. Bind the Ischecked property of the checkbox with the IsSelected property of the combobox.

<ComboBox.ItemTemplate>
    <DataTemplate>
        <CheckBox Content="{Binding Title}"
                  IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
                  Tag="{RelativeSource FindAncestor, 
                  AncestorType={x:Type ComboBox}}"
                 
                  />
    </DataTemplate>
</ComboBox.ItemTemplate>

Add a grid in the control template for the combobox and include a toggle button and a popup as below:

<ComboBox.Template>
    <ControlTemplate TargetType="ComboBox">              
        <Grid >
             <ToggleButton 
                        x:Name="ToggleButton" 
                       Grid.Column="2" IsChecked="
                       {Binding Path=IsDropDownOpen,Mode=TwoWay,
                       RelativeSource={RelativeSource TemplatedParent}}"
                        Focusable="false"                           
                        ClickMode="Press" HorizontalContentAlignment="Left" >
                        <ToggleButton.Template>
                            <ControlTemplate>
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="*"/>
                                        <ColumnDefinition Width="18"/>
                                    </Grid.ColumnDefinitions>
                                    <Border
                  x:Name="Border" 
                  Grid.ColumnSpan="2"
                  CornerRadius="2"
                  Background="White"
                  BorderBrush="Black"
                  BorderThickness="1,1,1,1" />
                                    <Border 
                    x:Name="BorderComp" 
                  Grid.Column="0"
                  CornerRadius="2" 
                  Margin="1" 
                 Background="White"
                  BorderBrush="Black"
                  BorderThickness="0,0,0,0" >
                                        <TextBlock Text="
                                        {Binding Path=Text,RelativeSource=
                                        {RelativeSource Mode=FindAncestor, 
                                        AncestorType=UserControl}}" 
                                               Background="White" 
                                               Padding="3" />
                                    </Border>
                                    <Path 
                  x:Name="Arrow"
                  Grid.Column="1"     
                  Fill="Black"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center"
                  Data="M 0 0 L 4 4 L 8 0 Z"/>
                                </Grid>
                            </ControlTemplate>
                        </ToggleButton.Template>
                         </ToggleButton>
            <Popup 
            Name="Popup"
            Placement="Bottom"                        
            AllowsTransparency="True" 
            Focusable="False" >
                <Grid 
                      Name="DropDown"
                      SnapsToDevicePixels="True" 
                    <Border 
                        x:Name="DropDownBorder"
                       BorderThickness="1" Background="White"
                                 BorderBrush="Black"/>
                    <ScrollViewer Margin="4,6,4,6" 
                    SnapsToDevicePixels="True" 
                    DataContext="{Binding}">
                        <StackPanel IsItemsHost="True" 
                        KeyboardNavigation.DirectionalNavigation=
                        "Contained" />
                    </ScrollViewer>
                </Grid>
            </Popup>
        </Grid>
    </ControlTemplate>
</ComboBox.Template>

We have added the combobox template, but still haven’t set the relationship between the popup and the toggle button. We are going to use the template binding concept here.

TemplateBinding is similar to normal data binding but it can be used only for templates. Here the toggle button acts as a templated parent and we are going to bind the IsChecked property of the toggle button to the popup by specifying a template binding for the IsOpen property of the popup. Once you see the below code, you can understand a little further about it.

Add the below property in the toggle button:

IsChecked="{Binding Path=IsDropDownOpen,
Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"

Add the below code in the popup:

IsOpen="{TemplateBinding IsDropDownOpen}"
PopupAnimation="Slide"

We used a common property IsDropDownOpen to set the binding for both the objects and we set it as two way in the toggle button so that whenever the popup is closed, the toggle button IsChecked property will be set as false. Also, we have set PopupAnimation as Slide.

We have to do one more thing now. The popup width should match the combobox width and the dropdown height should also be set.

Add the below properties as well inside the grid in the popup.

MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">

Let us add a few triggers to the combobox now before the control template closing tag.

  1. Whenever there are no items in the combobox, we have to set some minimum height for the popup.
  2. <ControlTemplate.Triggers>
        <Trigger Property="HasItems" Value="false">
            <Setter TargetName="DropDownBorder" 
            Property="MinHeight" Value="95"/>
        </Trigger>                    
    </ControlTemplate.Triggers>
  3. Set some corner radius for the dropdown popup:
  4. <Trigger SourceName="Popup" 
    Property="Popup.AllowsTransparency" Value="true">
        <Setter TargetName="DropDownBorder" 
        Property="CornerRadius"   Value="4"/>
        <Setter TargetName="DropDownBorder" 
        Property="Margin" Value="0,2,0,0"/>
    </Trigger>

Now we are almost done with the XAML page. Let us come back to the XAML if there are any style changes required.

The second step is adding the dependency property in the code behind. Most WPF developers already know about dependency properties. But for beginners, Dependency Property is a special kind of property where it gets value from the dependency object dynamically when we call the getvalue() method. When we set the value for a dependency property, it is not stored in field in an object, but in a dictionary of keys provided by the base class dependency object.

I am going to add four dependency properties:

  • ItemSource
  • SelectedItems
  • DefaultText
  • Text

These four properties are enough, but if you need any other dependency properties, then you can add your own.

public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(Dictionary<string, object>), 
            typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));

public static readonly DependencyProperty SelectedItemsProperty =
   DependencyProperty.Register("SelectedItems", 
   typeof(Dictionary<string, object>), 
   typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public static readonly DependencyProperty TextProperty =
   DependencyProperty.Register("Text", 
   typeof(string), typeof(MultiSelectComboBox), 
   new UIPropertyMetadata(string.Empty));

public static readonly DependencyProperty DefaultTextProperty =
    DependencyProperty.Register("DefaultText", typeof(string), 
    typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));

public Dictionary<string, object> ItemsSource
{
    get { return (Dictionary<string, 
    object>)GetValue(ItemsSourceProperty); }
    set
    {
        SetValue(ItemsSourceProperty, value);
    }
 }

public Dictionary<string, object> SelectedItems
{
    get { return (Dictionary<string, 
    object>)GetValue(SelectedItemsProperty); }
    set
    {
        SetValue(SelectedItemsProperty, value);             
    }
}

public string Text
{
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
}

public string DefaultText
{
    get { return (string)GetValue(DefaultTextProperty); }
    set { SetValue(DefaultTextProperty, value); }
}

Note: I have set both ItemSource and SelectedItems properties as a dictionary object because for binding to the combobox, a simple key value pair is enough. But you can use a list of objects instead of a dictionary.

Create a class as “Node” with two properties in the same namespace:

  • Title
  • IsSelected

Remember I already told you that we are binding the content property of the checkbox to Title.

public class Node
{
    public Node(string title)
    {
        Title = title;
    }
    
    public string Title { get; set; }
    public bool IsSelected { get; set; }
}

Now add an Observablecollection of class as _nodelist.

private ObservableCollection<Node> _nodeList;

Set the value inside the constructor for the above field.

_nodeList = new ObservableCollection<Node>();

Add a method DisplayInControl. This method will display the items in the dependency property ItemsSource.

private void DisplayInControl()
{
    _nodeList.Clear();
    if (this.ItemsSource.Count > 0)
        _nodeList.Add(new Node("All"));
    foreach (KeyValuePair<string, object> keyValue in this.ItemsSource)
    {
        Node node = new Node(keyValue.Key);
        _nodeList.Add(node);
    }
    MultiSelectCombo.ItemsSource = _nodeList;
}

Whenever there is more than one item, we add an extra value All. For each item inside the ItemsSource, we are adding the key to the nodelist. Finally, we set this nodelist as the ItemsSource for the combobox we named as MultiSelectCombo. But how are we going to use this method?? So in the dependency property, we have to set the valuechanged property. Rewrite the dependency property for the ItemSource like below:

public static readonly DependencyProperty ItemsSourceProperty =
   DependencyProperty.Register("ItemsSource", typeof(Dictionary<string,
   object>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
   new PropertyChangedCallback(MultiSelectComboBox.OnItemsSourceChanged)));

In the itemsSourceChanged event, call the method DisplayInControl.

private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    MultiSelectComboBox control = (MultiSelectComboBox)d;
    control.DisplayInControl();
}

We have added a PropertyChanged event. The next step will be whenever an item is checked in the dropdown. We have to set it as SelectedItems and also we have to display it in the combobox. If there is more than one value, we have to display the item in a comma separated format and if all the values are selected, we have to set All as the text in the dropdown as well as check all items.

Add a Click event in the checkbox:

<CheckBox Content="{Binding Title}"
      IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
      Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}"
      Click="CheckBox_Click" />

In the code behind, add logic to set the IsSelected property of each node in the nodelist to true if they are checked.

private void CheckBox_Click(object sender, RoutedEventArgs e)
{
    CheckBox clickedBox = (CheckBox)sender;

    if (clickedBox.Content == "All")
    {
        if (clickedBox.IsChecked.Value)
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = true;
                    }
                }
                else
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = false;
                    }
                }
    }
    else
    {
        int _selectedCount = 0;
        foreach (Node s in _nodeList)
        {
            if (s.IsSelected && s.Title != "All")
                _selectedCount++;
        }
        if (_selectedCount == _nodeList.Count - 1)
            _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = true;
        else
            _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = false;
    }
    SetSelectedItems();
}

Add the following method to set the selected items:

private void SetSelectedItems()
{
    if (SelectedItems == null)
        SelectedItems = new Dictionary<string, object>();
    SelectedItems.Clear();
    foreach (Node node in _nodeList)
    {
        if (node.IsSelected && node.Title != "All")
        {
            if (this.ItemsSource.Count > 0)

                SelectedItems.Add(node.Title, this.ItemsSource[node.Title]);
        }
    }
}

Even though we added set IsSelected =true for each of the nodes, still in the UI, when you check the “All” option, the other checkboxes are not getting checked. This is because we didn’t set the notify property changed event for the properties in the node class. Now we are revisiting Node class and we are implementing the INotifyPropertyChanged Interface.

public class Node : INotifyPropertyChanged
{
    private string _title;
    private bool _isSelected;
    #region ctor
    public Node(string title)
    {
        Title = title;
    }
    #endregion

    #region Properties
    public string Title
    {
        get
        {
            return _title;
        }
        set
        {
            _title = value;
            NotifyPropertyChanged("Title");
        }
    }
    public bool IsSelected
    {
        get
        {
            return _isSelected;
        }
        set
        {
            _isSelected = value;
            NotifyPropertyChanged("IsSelected");
        }
    }
    #endregion

    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

We are left with two tasks in our control now. Display the selected items in the combobox and when we load a page and there are some selected values, by default we have to select them to notify the user.

The below method will set the text in the toggle button content:

private void SetText()
{
    if (this.SelectedItems != null)
    {
        StringBuilder displayText = new StringBuilder();
        foreach (Node s in _nodeList)
        {
            if (s.IsSelected == true && s.Title == "All")
            {
                displayText = new StringBuilder();
                displayText.Append("All");
                break;
            }
            else if (s.IsSelected == true && s.Title != "All")
            {
                displayText.Append(s.Title);
                displayText.Append(',');
            }
        }
        this.Text = displayText.ToString().TrimEnd(new char[] { ',' }); 
    }           
    // set DefaultText if nothing else selected
    if (string.IsNullOrEmpty(this.Text))
    {
        this.Text = this.DefaultText;
    }
}

We have to call the above method (SetText) in the end of the checkbox click event so that whenever SelectedItems change, this method will be called.

Now add a method to set the IsSelected property of each node based on SelectedItems. We need this method to pre-populate the selected items on page load.

private void SelectNodes()
{
    foreach (KeyValuePair<string, object> keyValue in SelectedItems)
    {
        Node node = _nodeList.FirstOrDefault(i => i.Title == keyValue.Key);
        if (node != null)
            node.IsSelected = true;
    }
}

We have to modify the SelectedItemsProperty to include a property changed event and inside the event, call the selectNodes method and the SetText method.

So our dependency property will change to:

public static readonly DependencyProperty SelectedItemsProperty =
         DependencyProperty.Register
         ("SelectedItems", typeof(Dictionary<string, object>), 
         typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
         new PropertyChangedCallback
         (MultiSelectComboBox.OnSelectedItemsChanged)));

private static void OnSelectedItemsChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    MultiSelectComboBox control = (MultiSelectComboBox)d;
    control.SelectNodes();
    control.SetText();
}

Now we have added most of the essential things and it is time to use the control in the application.

Add a new WPF Application project and name it MultiSelectDemo. I am not going to explain the MVVM pattern here. You can find various articles about creating a simple WPF application using MVVM. Let me skip that part and directly take you to our control implementation. Add a viewmodelbase which has an InotifyProperyChanged Interface implementation. Add a viewmodel which inherits from ViewModelBase and it should have an Items and a SelectedItems dictionary property. To test our control, I have added the below code in my constructor:

Items = new Dictionary<string, object>();
            Items.Add("Chennai", "MAS");
            Items.Add("Trichy", "TPJ");
            Items.Add("Bangalore", "SBC");
            Items.Add("Coimbatore", "CBE");

            SelectedItems = new Dictionary<string, object>();
            SelectedItems.Add("Chennai", "MAS");
            SelectedItems.Add("Trichy", "TPJ");

Now add the WPF User control library project as reference to this application so that you will be able to see the user control library project namespace here. In the view, I included a reference for the user control library.

xmlns:control="clr-namespace:
MultiSelectComboBox;assembly=MultiSelectComboBox"

Inside the grid, I added our control with the following properties:

  • ItemsSource
  • SelectedItems
  • Width
  • Height

Remember we didn't set any Width and Height property in the control. This is because at different places we need different width and height. So I always suggest you add that in the view.

<control:MultiSelectComboBox 
Width="200" Height="30" 
  ItemsSource="{Binding Items}" 
  SelectedItems="{Binding SelectedItems}" 
  x:Name="MC" />

Now we can see our MultiSelectCombobox is working fine and we will be able to select different items. Also we can use this control in the code-behind bypassing MVVM. Let me also show you a sample of how to use this in the code-behind of the XAML file. We have to remove the ItemsSource and SelectedItems properties from the XAML. Add the same test data in the code-behind constructor.

Finally, set the ItemsSource and SelectedItems properties like below:

public MainWindow()
{
    InitializeComponent();
     Items = new Dictionary<string, object>();
    Items.Add("Chennai", "MAS");
    Items.Add("Trichy", "TPJ");
    Items.Add("Bangalore", "SBC");
    Items.Add("Coimbatore", "CBE");

    SelectedItems = new Dictionary<string, object>();
    SelectedItems.Add("Chennai", "MAS");
    SelectedItems.Add("Trichy", "TPJ");


    MC.ItemsSource = Items;
    MC.SelectedItems = SelectedItems;
}

Extension-1 (ToolTip)

In case if the width of the combobox is small and you have selected more items, you cannot see all the items in the text. So we have to set tooltip so that whenever user hovers the mouse over the combobox, he/she should be able to see all the selected items. In the below code, I have bind the text of the combobox to the tooltip.

 ToolTip="{Binding Path=Text, RelativeSource={RelativeSource Self}}" 
The combobox control in the XAML will look like this now:

 <control:MultiSelectComboBox Width="100" Height="30" 
ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}" 
x:Name="MC" ToolTip="{Binding Path=Text, RelativeSource={RelativeSource Self}}"/> 

Please check the attached demo files. Hope this document and the attachments will help you. Please share this document with your friends and provide your valuable feedback.

 

Extension -2 Unchecking "All" Item in the combobox 

 When you click on "All" option checkbox, all the items in that combobox should be checked. But the reverse scenario i.e unchecking "All" should uncheck all the selected items and it didn't happen. I have fixed this now using the below code in checkbox_click method. 

 We have to select all nodes if the clickedbox is checked and unselect all if the clickedbox is unchecked.

 private void CheckBox_Click(object sender, RoutedEventArgs e)
        {
            CheckBox clickedBox = (CheckBox)sender;
            if (clickedBox.Content == "All" )
            {
                if (clickedBox.IsChecked.Value)
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = true;
                    }
                }
                else
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = false;
                    }
                }
            }
            else
            {
                int _selectedCount = 0;
                foreach (Node s in _nodeList)
                {
                    if (s.IsSelected && s.Title != "All")
                        _selectedCount++;
                }
                if (_selectedCount == _nodeList.Count - 1)
                    _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = true;
                else
                    _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = false;
            }
            SetSelectedItems();
            SetText();
        } 

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here