Alexey Golub
WPF ListBox SelectedItems TwoWay binding

For some unclear reasons, WPF's ListBox control does not allow two-way binding on SelectedItems property the way it does with SelectedItem. This could have been very useful when using multiselect to bind the whole list of selected items to the model.

Interestingly, you can still call Add(), Remove(), Clear() methods on ListBox.SelectedItems which updates the selected items correctly, so it just comes down to implementing a behavior that makes the property bindable.

Behavior implementation

public class ListBoxSelectionBehavior : 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<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;
    }

    /// <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 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 view, OnListBoxSelectionChanged(…) method is called, where the changes are propagated to the model. This is simple, because all we need to do is copy the items from ListBox.SelectedItems to our own SelectedItems collection.

Usage

We can now use this behavior by initializing it in XAML, like this:

<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple">
    <i:Interaction.Behaviors>
        <behaviors:ListBoxSelectionBehavior 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 new reference.

This could also be very useful for multiselect, but unfortunately SelectedValues does not exist. Let's extend our behavior to support it.

public class ListBoxSelectionBehavior : 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<object>()
                    .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<object>()
                    .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 value of a property using an object and member path – it's used to establish conformity between items and values.

Usage with SelectedValuePath

Now we can specify SelectedValuePath in ListBox and our behavior will allow us to bind the SelectedValues property to model and vice versa.

<ListBox ItemsSource="{Binding Items}" SelectedValuePath="ID" SelectionMode="Multiple">
    <i:Interaction.Behaviors>
        <behaviors:ListBoxSelectionBehavior SelectedValues="{Binding SelectedValues}" />
    </i:Interaction.Behaviors>
    <ListBox.ItemTemplate>
        <!-- ... -->
    </ListBox.ItemTemplate>
</ListBox>