Introduction
I wanted to do a straight forward implementation of a ViewModel
with ItemsSource
and SelectedItems
. I could have just wrapped the ItemsSource ViewModel
in an Adapter
with an IsCheckedProperty
, but it seemed simpler to just have a SelectedItems
property. However, the ListBox SelectedItems
property is not a DependencyProperty
.
Design
I created a behavior with a SelectedItems
property:
public class SelectedItemsBahavior
{
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached("SelectedItems",
typeof(INotifyCollectionChanged), typeof(SelectedItemsBahavior),
new PropertyMetadata(default(IList), OnSelectedItemsChanged));
public static void SetSelectedItems(DependencyObject d, INotifyCollectionChanged value)
{
d.SetValue(SelectedItemsProperty, value);
}
public static IList GetSelectedItems(DependencyObject element)
{
return (IList)element.GetValue(SelectedItemsProperty);
}
private static void OnSelectedItemsChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
IList selectedItems = null;
void CollectionChangedEventHandler(object sender, NotifyCollectionChangedEventArgs args)
{
if (args.OldItems != null)
foreach (var item in args.OldItems)
if (selectedItems.Contains(item))
selectedItems.Remove(item);
if (args.NewItems != null)
foreach (var item in args.NewItems)
if (!selectedItems.Contains(item))
selectedItems.Add(item);
};
if (d is MultiSelector multiSelector)
{
selectedItems = multiSelector.SelectedItems;
multiSelector.SelectionChanged += OnSelectionChanged;
}
if (d is ListBox listBox)
{
selectedItems = listBox.SelectedItems;
listBox.SelectionMode = SelectionMode.Multiple;
listBox.SelectionChanged += OnSelectionChanged;
}
if (selectedItems == null) return;
if (e.OldValue is INotifyCollectionChanged)
(e.OldValue as INotifyCollectionChanged).CollectionChanged
-= collectionChangedEventHandler ;
if (e.NewValue is INotifyCollectionChanged)
(e.NewValue as INotifyCollectionChanged).CollectionChanged
+= collectionChangedEventHandler ;
}
private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var s = sender as DependencyObject;
if (!GetIsBusy(s))
{
SetIsBusy(s, true);
var list = GetSelectedItems((DependencyObject)sender);
foreach (var item in e.RemovedItems)
if (list.Contains(item)) list.Remove(item);
foreach (var item in e.AddedItems)
if (!list.Contains(item)) list.Add(item);
SetIsBusy(s, false);
}
}
private static readonly DependencyProperty IsBusyProperty =
DependencyProperty.RegisterAttached("IsBusy", typeof(bool),
typeof(SelectedItemsBahavior), new PropertyMetadata(default(bool)));
private static void SetIsBusy(DependencyObject element, bool value)
{
element.SetValue(IsBusyProperty, value);
}
private static bool GetIsBusy(DependencyObject element)
{
return (bool)element.GetValue(IsBusyProperty);
}
}
The design will support both a ListBox
and a MultiSelector
. The only difference is that class
that I Cast
to since both have the SelectedItems
property of type IList
which means both have the event SelectionChanged
event.
I have to handle changes in the control's SelectedItems
property and the IList
bound to the behavior's SelectedItems
DependencyProperty
to keep the two synchronized. That means handling the collection changed events for both collection.
The control's SelectionChanged
event
is handled by the OnSelectionChanged
event
handler which will synchronize any added or removed ItemsSource
items with the SelectedItems
DependencyProperty
based on the SelectionChangedEvenArgs
properties.
There is also a NotifyCollectionChangedEventHandler
that is defined in the DependencyProperty
changed event
handler. Note that a normal List
does not have a CollectionChanged
event, so if the ViewModel
needs to make changes to the SelectedItems
, then property that is bound to the SelectedItems
property must be of a Type
that is derived from the interface
INotifyCollectionChanged
such as ObservableCollection
.
If the property is of type INotifyCollectionChanged
, then the CollectionChanged
event
will be handled by the CollectionChangedEventHandler
, and this method will synchronize any changes with the control's SelectedItems
property.
Using the Code
Using this behavior is really easy, all that needs to be done is to use the behavior to bind to the property that is to contain the SelectedItems
collection in the ViewModel
:
<ListBox ItemsSource="{Binding ItemsSource}"
local:SelectedItemsBahavior.SelectedItems="{Binding SelectedItems}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Name="CheckBoxZone"
IsChecked="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},
Path=IsSelected}">
<TextBlock Width="{Binding ElementName=SizingBorder,
Path=ActualWidth}"
Foreground="Black"
Text="{Binding}"
TextWrapping="Wrap" />
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
A Bonus
It should be noticed that I used a DataTemplate
for the ListBox
defined above, and bound its IsChecked
property to the ListBoxItem IsSelected
property. This is a non invasive way to create a CheckBox
List in a ListBox
.
History
- 09/26/2017: Initial version