Search code examples
wpfcomboboxmulti-select

Multiselect WPF combo box


I have a WPF combo box which ItemsSource is ObservableCollection<User>, where User has string Name and bool IsChecked.

Also I have

<ComboBox.ItemTemplate>
    <DataTemplate>
        <StackPanel Orientation="Horizontal">
            <CheckBox IsChecked="{Binding IsChecked}" Width="20" />
            <TextBlock Text="{Binding Name}" />
         </StackPanel>
     </DataTemplate>
</ComboBox.ItemTemplate>

, which nicely shows check boxes before each name and allows me to check/uncheck users.

What I need is to make combo box selected item to show not the selected user but all checked usernames separated by comma, ideally (if resultant string is too long) with ellipsis in the middle, i. e. "Alice, Bart...mew, John".

Possible?


Solution

  • A little bit tricky to implement, but certainly can be done.

    First of all you'll want to replace the drop-down bar/button with a TextBlock, so you'll need to modify the control's template. Place the edit cursor anywhere inside the parent ComboBox declaration so that the control appears in the Properties panel at the bottom right. In properties, find Miscellaneous -> Template at the bottom, then click on the little down-arrow to the right and select "Convert to new resource". This will template out the control so that you can start editing it.

    Next, find the ToggleButton's ContentPresenter inside the ControlTemplate. You'll want to change it's Content binding to point to a property in your view models which I'll call Names:

    <!--<ContentPresenter x:Name="contentPresenter" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" Content="{TemplateBinding SelectionBoxItem}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>-->
    <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" Content="{Binding Names}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
    

    The next change is to also add a binding to your CheckBox Command property called "NamesChangedCommand" which will be called whenever the user changes the state of a CheckBox:

    <CheckBox IsChecked="{Binding IsChecked}" Width="20" Command="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=DataContext.NamesChangedCommand}" />
    

    Then back in your view model, all you have to do is implement this command and have it generate the new name list. Here's how you'd do that in MVVM Toolkit:

        [RelayCommand]
        public void NamesChanged()
        {
            this.Names = String.Join(", ",
                this.Items
                    .Where(item => item.IsChecked)
                    .Select(item => item.Name));
        }
    
        [ObservableProperty]
        private string names = "";
    

    Result:

    enter image description here