Update: Feb 2nd 2012 - I've updated this code to work with DataGrid.
Download the source code for this article
In the previous two blog posts in this series I explored the current best practises for dealing with multiple selections in MVVM, and explained the problems one may encounter.
My feeling for a long time has been “What I really need is a CollectionView that supports multiple selections.” So I decided to write one, and it turned out to be pretty easy!
So without delay, here’s the code for MultiSelectCollectionView:
public class MultiSelectCollectionView<T> : ListCollectionView, IMultiSelectCollectionView { public MultiSelectCollectionView(IList list) : base(list) { SelectedItems = new ObservableCollection<T>(); } void IMultiSelectCollectionView.AddControl(Selector selector) { this.controls.Add(selector); SetSelection(selector); selector.SelectionChanged += control_SelectionChanged; } void IMultiSelectCollectionView.RemoveControl(Selector selector) { if (this.controls.Remove(selector)) { selector.SelectionChanged -= control_SelectionChanged; } } public ObservableCollection<T> SelectedItems { get; private set; } void SetSelection(Selector selector) { MultiSelector multiSelector = selector as MultiSelector; ListBox listBox = selector as ListBox; if (multiSelector != null) { multiSelector.SelectedItems.Clear(); foreach (T item in SelectedItems) { multiSelector.SelectedItems.Add(item); } } else if (listBox != null) { listBox.SelectedItems.Clear(); foreach (T item in SelectedItems) { listBox.SelectedItems.Add(item); } } } void control_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (!this.ignoreSelectionChanged) { bool changed = false; this.ignoreSelectionChanged = true; try { foreach (T item in e.AddedItems) { if (!SelectedItems.Contains(item)) { SelectedItems.Add(item); changed = true; } } foreach (T item in e.RemovedItems) { if (SelectedItems.Remove(item)) { changed = true; } } if (changed) { foreach (Selector control in this.controls) { if (control != sender) { SetSelection(control); } } } } finally { this.ignoreSelectionChanged = false; } } } bool ignoreSelectionChanged; List<Selector> controls = new List<Selector>(); }
As you can see, it’s pretty simple. The class derives from ListCollectionView and maintains an ObservableCollection to hold the selected items and a List to hold the controls that are bound to it.
One thing to notice is that the class is generic, in order to allow the SelectedItems collection to be strongly typed. This may seem like a strange choice considering that CollectionViews are usually not strongly typed.
The reason for this is that the items in a CollectionView will usually only be used by the View side of the application and view controls do not care about having strongly typed values. However, the new SelectedItems collection will usually be used from the ViewModel side of things, where having strongly typed values is important.
Now, WPF controls have no knowledge of MultiSelectCollectionView so if you hook it up to an ItemsControl now, it will work exactly the same as a ListCollectionView, with no multi-select support. We add in multi-select support using the magic of attached properties:
public static class MultiSelect { static MultiSelect() { Selector.ItemsSourceProperty.OverrideMetadata(typeof(Selector), new FrameworkPropertyMetadata(ItemsSourceChanged)); } public static bool GetIsEnabled(Selector target) { return (bool)target.GetValue(IsEnabledProperty); } public static void SetIsEnabled(Selector target, bool value) { target.SetValue(IsEnabledProperty, value); } public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(MultiSelect), new UIPropertyMetadata(IsEnabledChanged)); static void IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) { Selector selector = sender as Selector; IMultiSelectCollectionView collectionView = selector.ItemsSource as IMultiSelectCollectionView; if (selector != null && collectionView != null) { if ((bool)e.NewValue) { collectionView.AddControl(selector); } else { collectionView.RemoveControl(selector); } } } static void ItemsSourceChanged(object sender, DependencyPropertyChangedEventArgs e) { Selector selector = sender as Selector; if (GetIsEnabled(selector)) { IMultiSelectCollectionView oldCollectionView = e.OldValue as IMultiSelectCollectionView; IMultiSelectCollectionView newCollectionView = e.NewValue as IMultiSelectCollectionView; if (oldCollectionView != null) { oldCollectionView.RemoveControl(selector); } if (newCollectionView != null) { newCollectionView.AddControl(selector); } } } }
Here we create an attached property, MultiSelect.IsEnabled. When set to true on a control derived from Selector, it adds the control to the MultiSelectCollectionView’s list of bound controls.
We also override the metadata for the ItemsSource property for all Selector controls. In this way we can detect when a control’s ItemsSource changes and ensure that the control is registered with the correct MultiSelectCollectionView. Note that because MultiSelectCollectionView is generic, we use the non-generic IMultiSelectCollectionView interface to add/remove controls. IMultiSelectCollectionView looks like this:
public interface IMultiSelectCollectionView { void AddControl(Selector selector); void RemoveControl(Selector selector); }
Now we can add multiple selection capabilites to a control like so:
<ListBox ItemsSource="{Binding Items}" SelectionMode="Extended" local:MultiSelect.IsEnabled="True"/>
That’s it! You can download the source code here. Let me know of any problems you find and also any improvements!