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]()