Multi Select ComboBox in WPF

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 don't want to use a listbox and also we are not interested in third-party tools. Instead of that, we wanted to use a multiselect combobox. When I browse through various blogs, forums, etc. I got some good code, but none of them works with the MVVM pattern. In those articles, most of the datasource binding is done in code behind. So I have made changes to that existing code to support MVVM. In this article I explain how to 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 to 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 MultiSelect Combo Box ,we must 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 checkboxes. Since we are going to write our custom data template for combobox items, we can't just directly use combobox. What we can do is we must design the template of the combo box as well, apart from defining the item template.

Our combobox should look like the following one.

WPF.jpg

In order to do this, as I mentioned in the picture above, we need a toggle button to determine when to open/close the dropdown 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. Using this multiselect combobox dll in some other XAML application.

To define the styles and templates, remove the grid tags and add a combobox to 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 combobox above. The item template should be a checkbox. Bind the content property of your checkbox to the property "Title". Remember we didn't start with code behind. We must set this property in Codebehind. 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 to the control template for the combobox and include a toggle button, and a popup as in the following.
 

<ComboBox.Template>

 

            <ControlTemplate TargetType="ComboBox">             

                    <Grid >

                        <ToggleButton

                        Name="ToggleButton"

Content="{Binding Path=Text,Mode=TwoWay,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"

                       Grid.Column="2"

    Focusable="false"                       

                        ClickMode="Press">

                        </ToggleButton>

                        <Popup

                        Name="Popup"

                        Placement="Bottom"                       

                        AllowsTransparency="True"

                        Focusable="False" >

                            <Grid

                                  Name="DropDown"

                                  SnapsToDevicePixels="True"

                                <Border

                                    x:Name="DropDownBorder"

                                   BorderThickness="1"

                                    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 we didn't set the relationship between popup and toggle button. We will 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 templatedparent and we will 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 following code, you can understand it a little better.

Add the following property to the togglebutton:

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

Add the following code to 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 a two-way in 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 must do one more thing now. The popup width should match the combobox width and dropdownheight also should be set.

Add the following 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 controltemplate closing tag.

1. Whenever there are no items in the combobox , we must set some minimum height for the popup, as in:

<ControlTemplate.Triggers>
                    <Trigger Property="HasItems" Value="false">
 <Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
                    </Trigger>                   
      
</ControlTemplate.Triggers>


2. Set some corner radius for the dropdown popup:
 

  <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 return to the XAML if there are any style changes required.

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

I am adding the following four dependency properties:

  1. ItemSource

  2. SelectedItems

  3. DefaultText

  4. 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 the 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.

  1. Title

  2. IsSelected.

Remember, I already explained that we are binding the content property of the checkbox to the "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 field above.

_nodeList = new ObservableCollection<Node>();

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

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 item source, we are adding the key to the nodelist. Finally we set this nodelist as itemsource for the combobox,we named it MultiSelectCombo. But how are we going to use this method?? So in the dependency property we must set the valuechanged property. Rewrite the dependency property for ItemSource as in the following:
 

public static readonly DependencyProperty ItemsSourceProperty =

            DependencyProperty.Register("ItemsSource", typeof(Dictionary<string, object>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,

       new PropertyChangedCallback(MultiSelectComboBox.OnItemsSourceChanged)));


In itemsSourceChanged event , call this 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 for whenever an item is checked in the dropdown, we must set it as selecteditems and also we must display it in the combobox. If there are more than one values, we must display the item in a comma separated format and if all the values are selected, we must set "All" as the text in the dropdown as well as check all the items.

Add a Click event to the Checkbox, as in:
 

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")

            {

                foreach (Node node in _nodeList)

                {

                    node.IsSelected = true;

                }

            }

            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 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 "All" options, the other checkboxes are not being checked. This is because we didn't set the notify property changed event for the properties in the node Class. Now we are revisiting the 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 a load page and there are some selectedvalues, by default we must select them to notify the user.

The following 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 must call the preceding method (SetText) in the end of the Checkbox click event. So that whenever selecteditems are changed, this method will be called.

Now add a method to set the IsSelected property of each node based on the selecteditems. We need this method to prepopulate 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 must modify the SelectedItemsProperty to include a property changed event and inside the event, call the selectNodes method and SetText method.

So our dependency property will be changed 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's time to use the control in the application.

Add a new WPF Application project and name it as 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 that has an InotifyProperyChanged Interface implementation. Add a viewmodel that inherits from ViewModelBase and it should have Items and SelectedItems dictionary properties. To test our control, I have added the following 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 a reference to this application. So that you will be able to see the user control library project namespace here. In the view

I included reference for the user control library.

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

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

Items
SelectedItems
Width
Height

Remember, we didn't set any width and height properties in the control. Because at various places we need different widths and heights. So I always suggest that you to add then 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 multiple items.

Also we can use this control in the code behind bying MVVM. Let me also show you a sample of how to use this in the code behind of a XAML file.

We must remove the Items and Selecteditems properties from the XAML. Add the same test data in the code behind constructor.

Finally set the Items and SelectedItems properties as in the following:
 

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;

        }

 

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.