My working MVVM approach for multiselection WPF ComboBox.
In xaml I use Combobox and IsHitTestVisible="False" TextBlock with joined values which are checked in ComboBox. I used Grid container because we need to have TextBlock under ComboBox.
<Grid>
<ComboBox ItemsSource="{Binding ComboItemsCollection}"
IsReadOnly="True"
IsEditable="True">
<b:Interaction.Behaviors>
<local:PreventSelectionBehavior/>
</b:Interaction.Behaviors>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsSelected}" Width="Auto" Content="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding ComboItemsCollectionDisplayString}"
IsHitTestVisible="False"/>
</Grid>
My ViewModels look like
public class ComboItemVm : INotifyPropertyChanged
{
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (value == _isSelected)
return;
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
private string _name;
public string Name
{
get => _name;
set
{
if (value == _name)
return;
_name = value;
OnPropertyChanged(nameof(Name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public class WpfTestVm : INotifyPropertyChanged
{
public string ComboItemsCollectionDisplayString
=> string.Join(", ", ComboItemsCollection
.Where(item => item.IsSelected)
.Select(item => item.Name));
public ObservableCollectionEx<ComboItemVm> ComboItemsCollection { get; set; } = new ObservableCollectionEx<ComboItemVm>
{
new ComboItemVm{Name = "Item1"},
new ComboItemVm{Name = "Item2"},
new ComboItemVm{Name = "Item3"},
};
public WpfTestVm()
{
ComboItemsCollection.ItemChanged += (sender, args) =>
{
OnPropertyChanged(nameof(ComboItemsCollectionDisplayString));
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Extended observable collection implementation for tracking changes of IsSelected in Collection's items.
public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public ObservableCollectionEx(IEnumerable<T> initialData) : base(initialData)
{
Init();
}
public ObservableCollectionEx()
{
Init();
}
private void Init()
{
foreach (T item in Items)
item.PropertyChanged += ItemOnPropertyChanged;
CollectionChanged += OnObservableCollectionCollectionChanged;
}
private void OnObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (T item in e.NewItems)
{
if (item != null)
item.PropertyChanged += ItemOnPropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (T item in e.OldItems)
{
if (item != null)
item.PropertyChanged -= ItemOnPropertyChanged;
}
}
}
private void ItemOnPropertyChanged(object sender, PropertyChangedEventArgs e)
=> ItemChanged?.Invoke(sender, e);
public event PropertyChangedEventHandler ItemChanged;
}
And behavior which prevent Selection in ComboBox, because we don't want it.
public class PreventSelectionBehavior : Behavior<ComboBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
}
private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox element = sender as ComboBox;
if (element == null)
return;
element.SelectedItem = null;
}
}
I tryied different approached, I tryied to change ComboBox template, but this approach is the best for me.