WPF customize MultiSelectComboBox based on combobox

public class MultiSelectComboBox : ComboBox
{
    public IList SelectedItems
    {
        get { return (IList)GetValue(SelectedItemsProperty); }
        set { SetValue(SelectedItemsProperty, value); }
    }

    // Using a DependencyProperty as the backing store for SelectedItems.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register(
            nameof(SelectedItems),
            typeof(IList),
            typeof(MultiSelectComboBox),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnSelectedItemsChanged));

    private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if(d is MultiSelectComboBox control)
        {
            control.UpdateDisplayText();
        }
    }


    public string DisplayText
    {
        get { return (string)GetValue(DisplayTextProperty); }
        set { SetValue(DisplayTextProperty, value); }
    }

    // Using a DependencyProperty as the backing store for DisplayText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DisplayTextProperty =
        DependencyProperty.Register(
            nameof(DisplayText), 
            typeof(string),
            typeof(MultiSelectComboBox),
            new PropertyMetadata(string.Empty));



    public bool KeepDropDownOpen
    {
        get { return (bool)GetValue(KeepDropDownOpenProperty); }
        set { SetValue(KeepDropDownOpenProperty, value); }
    }

    // Using a DependencyProperty as the backing store for KeepDropDownOpen.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty KeepDropDownOpenProperty =
        DependencyProperty.Register(
            nameof(KeepDropDownOpen),
            typeof(bool), 
            typeof(MultiSelectComboBox),
            new PropertyMetadata(false));


    public MultiSelectComboBox()
    {
        SelectedItems = new System.Collections.ObjectModel.ObservableCollection<object>();
        this.SelectionChanged += OnMultiSelectionChanged;
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        //this.SelectedItem = null;
        e.Handled = true;
    }

    private void OnMultiSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        UpdateDisplayText();
    }

    public void UpdateDisplayText()
    {
        if(SelectedItems==null || SelectedItems.Count==0)
        {
            DisplayText = "Please select...";
            return;
        }

        var displayList = new ObservableCollection<string>();

        foreach(var item in SelectedItems)
        {
            string text = GetItemDisplayText(item);
            displayList.Add(text);
        }

        DisplayText = string.Join(". ", displayList);
    }

    private string GetItemDisplayText(object item)
    {
        if(item==null)
        {
            return string.Empty;
        }

        if(!string.IsNullOrWhiteSpace(this.DisplayMemberPath))
        {
            var prop = item.GetType().GetProperty(this.DisplayMemberPath);
            return prop?.GetValue(item)?.ToString() ?? item.ToString();
        }
        return item.ToString();
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if(GetTemplateChild("PART_Popup") is Popup popup)
        {
            popup.Closed += (s, e) =>
            {
                if (!KeepDropDownOpen)
                {
                    UpdateDisplayText();
                }
            };                
        }
    }
}

[ValueConversion(typeof(object), typeof(bool))]
public class SelectedItemToBoolConverter : MarkupExtension, IMultiValueConverter
{
    private static SelectedItemToBoolConverter _instance;

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length < 2 || values[0] == null || values[1] == null)
        {
            return false;
        }                

        var item = values[0];
        var selectedList = values[1] as IList;
        if(selectedList==null)
        {
            return false;
        }

        return selectedList.Contains(item);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new object[]
        {
            Binding. DoNothing,
            Binding.DoNothing
        };
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return _instance ?? (_instance = new SelectedItemToBoolConverter());
    }
}

 

 

 

<Window x:Class="WpfApp33.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp33"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <Style TargetType="local:MultiSelectComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:MultiSelectComboBox">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                SnapsToDevicePixels="True">
                            <Grid>
                                <ToggleButton x:Name="ToggleButton"
                                            ClickMode="Press"
                                            Focusable="False"
                                            IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
                                            Content="{TemplateBinding DisplayText}"
                                            HorizontalContentAlignment="Left"
                                            Padding="3,0,0,0"/>
                   
                                <Popup x:Name="PART_Popup"
                                       AllowsTransparency="True"
                                       IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}"
                                       Placement="Bottom"
                                       PopupAnimation="Slide">
                                    <Border x:Name="DropDownBorder"
                                            Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
                                            BorderThickness="1"
                                            BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}"
                                            MaxHeight="{TemplateBinding MaxDropDownHeight}"
                                            MinWidth="{TemplateBinding ActualWidth}">
                                        <ScrollViewer CanContentScroll="True">
                                            <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                        </ScrollViewer>
                                    </Border>
                                </Popup>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>

            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <CheckBox Content="{Binding}"
                                  Margin="2"
                                  Click="CheckBoxItem_Click">
                            <CheckBox.IsChecked>
                                <MultiBinding Converter="{local:SelectedItemToBoolConverter}"
                                              Mode="TwoWay">
                                    <Binding Path="."/>
                                    <Binding Path="SelectedItems" RelativeSource="{RelativeSource AncestorType=local:MultiSelectComboBox}"/>
                                </MultiBinding>
                            </CheckBox.IsChecked>
                        </CheckBox>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Window.DataContext>
        <local:MainVM/>
    </Window.DataContext>
    <Grid Margin="20">
        <StackPanel VerticalAlignment="Center">          
            <local:MultiSelectComboBox x:Name="multiCombo" 
                                       ItemsSource="{Binding BooksCollection}"
                                      Height="130"
                                      Width="1200"
                                      DisplayText="请选择..."/>         
        </StackPanel>
    </Grid>
</Window>


using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp33
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        ///  
        /// </summary>
        private void CheckBoxItem_Click(object sender, RoutedEventArgs e)
        {
            if (sender is CheckBox cb && cb.DataContext != null)
            {
                var item = cb.DataContext;
                var selectedList = multiCombo.SelectedItems;

                if (cb.IsChecked == true)
                {
                    if (!selectedList.Contains(item))
                        selectedList.Add(item);
                }
                else
                {
                    if (selectedList.Contains(item))
                        selectedList.Remove(item);
                }
                 
                multiCombo.UpdateDisplayText();
            }
        }
    }

    public class MainVM : INotifyPropertyChanged
    {
        public MainVM()
        {
            BooksCollection = new ObservableCollection<Book>();
            for(int i=1;i<11;i++)
            {
                BooksCollection.Add(new Book()
                {
                    Id = i,
                    Name = $"Name_{i}",
                    ISBN = $"ISBN_{i}",
                    Comment = $"Comment_{i}",
                    Content = $"Content_{i}"
                });
            }
        }

        private ObservableCollection<Book> booksCollection;
        public ObservableCollection<Book> BooksCollection
        {
            get
            {
                return booksCollection;
            }
            set
            {
                if(value!=booksCollection)
                {
                    booksCollection = value;
                    OnPropertyChanged();
                }
            }
        }
        public event PropertyChangedEventHandler? PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string propName="")
        {
            var handler = PropertyChanged;
            handler?.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    }

    public class Book
    {
        public int Id { get; set; } 
        public string Name { get; set; }
        public string ISBN { get; set;  }
        public string Comment { get; set;  }
        public string Content { get; set; }

        public override string ToString()
        {
            return $"Id:{Id},Name:{Name},ISBN:{ISBN}";
        }
    }

    public class MultiSelectComboBox : ComboBox
    {
        public IList SelectedItems
        {
            get { return (IList)GetValue(SelectedItemsProperty); }
            set { SetValue(SelectedItemsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedItems.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedItemsProperty =
            DependencyProperty.Register(
                nameof(SelectedItems),
                typeof(IList),
                typeof(MultiSelectComboBox),
                new FrameworkPropertyMetadata(
                    null,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    OnSelectedItemsChanged));

        private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if(d is MultiSelectComboBox control)
            {
                control.UpdateDisplayText();
            }
        }


        public string DisplayText
        {
            get { return (string)GetValue(DisplayTextProperty); }
            set { SetValue(DisplayTextProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DisplayText.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DisplayTextProperty =
            DependencyProperty.Register(
                nameof(DisplayText), 
                typeof(string),
                typeof(MultiSelectComboBox),
                new PropertyMetadata(string.Empty));



        public bool KeepDropDownOpen
        {
            get { return (bool)GetValue(KeepDropDownOpenProperty); }
            set { SetValue(KeepDropDownOpenProperty, value); }
        }

        // Using a DependencyProperty as the backing store for KeepDropDownOpen.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty KeepDropDownOpenProperty =
            DependencyProperty.Register(
                nameof(KeepDropDownOpen),
                typeof(bool), 
                typeof(MultiSelectComboBox),
                new PropertyMetadata(false));


        public MultiSelectComboBox()
        {
            SelectedItems = new System.Collections.ObjectModel.ObservableCollection<object>();
            this.SelectionChanged += OnMultiSelectionChanged;
        }

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            //this.SelectedItem = null;
            e.Handled = true;
        }

        private void OnMultiSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            UpdateDisplayText();
        }

        public void UpdateDisplayText()
        {
            if(SelectedItems==null || SelectedItems.Count==0)
            {
                DisplayText = "Please select...";
                return;
            }

            var displayList = new ObservableCollection<string>();

            foreach(var item in SelectedItems)
            {
                string text = GetItemDisplayText(item);
                displayList.Add(text);
            }

            DisplayText = string.Join(". ", displayList);
        }

        private string GetItemDisplayText(object item)
        {
            if(item==null)
            {
                return string.Empty;
            }

            if(!string.IsNullOrWhiteSpace(this.DisplayMemberPath))
            {
                var prop = item.GetType().GetProperty(this.DisplayMemberPath);
                return prop?.GetValue(item)?.ToString() ?? item.ToString();
            }
            return item.ToString();
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if(GetTemplateChild("PART_Popup") is Popup popup)
            {
                popup.Closed += (s, e) =>
                {
                    if (!KeepDropDownOpen)
                    {
                        UpdateDisplayText();
                    }
                };                
            }
        }
    }

    [ValueConversion(typeof(object), typeof(bool))]
    public class SelectedItemToBoolConverter : MarkupExtension, IMultiValueConverter
    {
        private static SelectedItemToBoolConverter _instance;

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Length < 2 || values[0] == null || values[1] == null)
            {
                return false;
            }                

            var item = values[0];
            var selectedList = values[1] as IList;
            if(selectedList==null)
            {
                return false;
            }

            return selectedList.Contains(item);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return new object[]
            {
                Binding. DoNothing,
                Binding.DoNothing
            };
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return _instance ?? (_instance = new SelectedItemToBoolConverter());
        }
    }
}

 

 

image

 

 

 

 

 

 

image

 

posted @ 2026-05-21 15:54  FredGrit  阅读(7)  评论(0)    收藏  举报