WPF ListBox SelectedItems TwoWay Binding • Oleksii Holub

For some unclear reasons, WPF’s ListBox control does not allow two-way binding on the SelectedItems property the way it does with SelectedItem. This could have been very useful when using multi-select to bind the whole list of selected items to the view model.
Interestingly, you can still call Add(), Remove(), Clear() methods on ListBox.SelectedItems which updates the selection correctly, so it just comes down to implementing a behavior that makes the property bindable.
Behavior implementation
Here’s the behavior that allows two-way binding on SelectedItems:
public class ListBoxSelectionBehavior<T> : Behavior<ListBox>
{
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(
nameof(SelectedItems),
typeof(IList),
typeof(ListBoxSelectionBehavior),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnSelectedItemsChanged
)
);

private static void OnSelectedItemsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var behavior = (ListBoxSelectionBehavior) sender;
if (behavior._modelHandled) return;

if (behavior.AssociatedObject == null)
return;

behavior._modelHandled = true;
behavior.SelectItems();
behavior._modelHandled = false;
}

private bool _viewHandled;
private bool _modelHandled;

public IList SelectedItems
{
get => (IList) GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}

// Propagate selected items from model to view
private void SelectItems()
{
_viewHandled = true;
AssociatedObject.SelectedItems.Clear();
if (SelectedItems != null)
{
foreach (var item in SelectedItems)
AssociatedObject.SelectedItems.Add(item);
}
_viewHandled = false;
}

// Propagate selected items from view to model
private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs args)
{
if (_viewHandled) return;
if (AssociatedObject.Items.SourceCollection == null) return;

SelectedItems = AssociatedObject.SelectedItems.Cast<T>().ToArray();
}

// Re-select items when the set of items changes
private void OnListBoxItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (_viewHandled) return;
if (AssociatedObject.Items.SourceCollection == null) return;

SelectItems();
}

protected override void OnAttached()
{
base.OnAttached();

AssociatedObject.SelectionChanged += OnListBoxSelectionChanged;
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged += OnListBoxItemsChanged;
}

/// <inheritdoc />
protected override void OnDetaching()
{
base.OnDetaching();

if (AssociatedObject != null)
{
AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged;
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged -= OnListBoxItemsChanged;
}
}
}

The behavior above defines its own SelectedItems property, identical to the one in ListBox, except it can be bound to and is not read-only.
When the property is changed from the view model, the OnSelectedItemsChanged(…) method is called, which is where the changes are propagated to the view. We do that in the SelectItems() method where we just clear and add new items to the ListBox.SelectedItems collection.
When the change is triggered by the view, we call the OnListBoxSelectionChanged(…) method. To update the selected items on the view model, we copy the items from ListBox.SelectedItems to our own SelectedItems collection.
Note that this behavior is generic because we expect to be able to bind to a collection of an arbitrary type on the view model’s side. WPF doesn’t support generic behaviors, however, so we have to subtype this class for each specific data type:
public class MyObjectListBoxSelectionBehavior : ListBoxSelectionBehavior<MyObject>
{
}

Usage
We can now use this behavior by initializing it in XAML, like this:
<ListBox ItemsSource=”{Binding Items}” SelectionMode=”Multiple”>
<i:Interaction.Behaviors>
<behaviors:MyObjectListBoxSelectionBehavior SelectedItems=”{Binding SelectedItems}” />
</i:Interaction.Behaviors>
<ListBox.ItemTemplate>
<!– … –>
</ListBox.ItemTemplate>
</ListBox>

Adding support for SelectedValuePath
Another useful feature of ListBox is that you can make a binding proxy using SelectedValuePath and SelectedValue. Setting SelectedValuePath lets you specify a member path to be evaluated by SelectedValue.
The great part about it is that it also works the other way around — changing SelectedValue will use the member path in SelectedValuePath to update SelectedItem with a new reference.
This could also be very useful for multi-select, but unfortunately the plural version, SelectedValues, does not exist. Let’s extend our behavior to add support for it.
public class ListBoxSelectionBehavior<T> : Behavior<ListBox>
{
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(
nameof(SelectedItems),
typeof(IList),
typeof(ListBoxSelectionBehavior),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnSelectedItemsChanged
)
);

public static readonly DependencyProperty SelectedValuesProperty =
DependencyProperty.Register(
nameof(SelectedValues),
typeof(IList),
typeof(ListBoxSelectionBehavior),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnSelectedValuesChanged
)
);

private static void OnSelectedItemsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var behavior = (ListBoxSelectionBehavior) sender;
if (behavior._modelHandled) return;

if (behavior.AssociatedObject == null)
return;

behavior._modelHandled = true;
behavior.SelectedItemsToValues();
behavior.SelectItems();
behavior._modelHandled = false;
}

private static void OnSelectedValuesChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var behavior = (ListBoxSelectionBehavior) sender;
if (behavior._modelHandled) return;

if (behavior.AssociatedObject == null)
return;

behavior._modelHandled = true;
behavior.SelectedValuesToItems();
behavior.SelectItems();
behavior._modelHandled = false;
}

private static object GetDeepPropertyValue(object obj, string path)
{
if (string.IsNullOrWhiteSpace(path)) return obj;
while (true)
{
if (path.Contains(‘.’))
{
string[] split = path.Split(‘.’);
string remainingProperty = path.Substring(path.IndexOf(‘.’) + 1);
obj = obj.GetType().GetProperty(split[0]).GetValue(obj, null);
path = remainingProperty;
continue;
}
return obj.GetType().GetProperty(path).GetValue(obj, null);
}
}

private bool _viewHandled;
private bool _modelHandled;

public IList SelectedItems
{
get => (IList) GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}

public IList SelectedValues
{
get => (IList) GetValue(SelectedValuesProperty);
set => SetValue(SelectedValuesProperty, value);
}

// Propagate selected items from model to view
private void SelectItems()
{
_viewHandled = true;
AssociatedObject.SelectedItems.Clear();
if (SelectedItems != null)
{
foreach (var item in SelectedItems)
AssociatedObject.SelectedItems.Add(item);
}
_viewHandled = false;
}

// Update SelectedItems based on SelectedValues
private void SelectedValuesToItems()
{
if (SelectedValues == null)
{
SelectedItems = null;
}
else
{
SelectedItems =
AssociatedObject.Items.Cast<T>()
.Where(i => SelectedValues.Contains(GetDeepPropertyValue(i, AssociatedObject.SelectedValuePath)))
.ToArray();
}
}

// Update SelectedValues based on SelectedItems
private void SelectedItemsToValues()
{
if (SelectedItems == null)
{
SelectedValues = null;
}
else
{
SelectedValues =
SelectedItems.Cast<T>()
.Select(i => GetDeepPropertyValue(i, AssociatedObject.SelectedValuePath))
.ToArray();
}
}

// Propagate selected items from view to model
private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs args)
{
if (_viewHandled) return;
if (AssociatedObject.Items.SourceCollection == null) return;

SelectedItems = AssociatedObject.SelectedItems.Cast<object>().ToArray();
}

// Re-select items when the set of items changes
private void OnListBoxItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (_viewHandled) return;
if (AssociatedObject.Items.SourceCollection == null) return;

SelectItems();
}

protected override void OnAttached()
{
base.OnAttached();

AssociatedObject.SelectionChanged += OnListBoxSelectionChanged;
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged += OnListBoxItemsChanged;

_modelHandled = true;
SelectedValuesToItems();
SelectItems();
_modelHandled = false;
}

/// <inheritdoc />
protected override void OnDetaching()
{
base.OnDetaching();

if (AssociatedObject != null)
{
AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged;
((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged -= OnListBoxItemsChanged;
}
}
}

I added another dependency property for SelectedValues and a few new methods.
SelectedValuesToItems() and SelectedItemsToValues() convert between SelectedItems and SelectedValues, depending on which property was updated. GetDeepPropertyValue(…) is used to extract the value of the property using an object and a member path, to establish conformity between selected items and their values.
Usage with SelectedValuePath
Now we can specify SelectedValuePath in ListBox and our behavior will allow us to bind the SelectedValues property to the model and vice versa.
<ListBox ItemsSource=”{Binding Items}” SelectedValuePath=”ID” SelectionMode=”Multiple”>
<i:Interaction.Behaviors>
<behaviors:MyObjectListBoxSelectionBehavior SelectedValues=”{Binding SelectedValues}” />
</i:Interaction.Behaviors>
<ListBox.ItemTemplate>
<!– … –>
</ListBox.ItemTemplate>
</ListBox>

Share this
Tags

Must-read

N.W.T. leaders worry wildfires, low water will mean even longer delay for much-needed housing units

Seniors in five N.W.T. communities expecting to move into new homes earlier this spring will now have to wait until at least summer, because of wildfires...

PC candidate says comments on recruiting doctors from India and Pakistan weren’t meant to be derogatory

Lin Paddock, the Progressive Conservative candidate in the upcoming Baie Verte-Green Bay byelection, said Tuesday that comments he made about recruiting doctors in India...

Repairs near completion at Rissers Beach Provincial Park after storm damage

Eight months after post-tropical storm Lee tore through Rissers Beach Provincial Park on Nova Scotia's South Shore, repairs are moving into the final stages.  Rissers...

Recent articles

More like this

LEAVE A REPLY

Please enter your comment!
Please enter your name here