22

I have two views of some data: a list view (a ListBox now, but I've been meaning to switch to ListView) and a fancy graphical representation on a map. In either view the user can click an object and it will be selected in both views. Multiselect is also possible, so each ViewModel instance has its own IsSelected property.

Currently I'm binding ListBoxItem.IsSelected to ViewModel.IsSelected, but this only works properly if the ListBox is NOT virtualizing (see here). Unfortunately, disabling virtualization hurts performance and my app has become too slow.

So I have to enable virtualization again. In order to maintain the ViewModel.IsSelected property of off-screen items, I noticed that ListBox and ListView have a SelectionChanged event that I can (presumably) use to propagate the selection state from the ListBox/ListView to the ViewModel.

My question is, how do I propagate selection state in the reverse direction? The SelectedItems property of ListBox/ListView is read-only! Suppose the user clicks an item in the graphical representation, but it is off-screen w.r.t. the list. If I just set ViewModel.IsSelected then the ListBox/ListView will be unaware of the new selection, and as a consequence it will fail to deselect that item if the user clicks a different item in the list. I could call ListBox.ScrollIntoView from the ViewModel, but there are a couple of problems:

  • In my UI it's actually possible to select two items with one click if they are in the same location graphically, although they may be located in completely different locations in the ListBox/ListView.
  • It breaks ViewModel isolation (my ViewModel is totally unaware of WPF and I'd like to keep it that way.)

So, my dear WPF experts, any thoughts?

EDIT: I ended up switching to an Infragistics control and using an ugly and rather slow solution. The point is, I no longer need an answer.

Community
  • 1
  • 1
Qwertie
  • 16,354
  • 20
  • 105
  • 148
  • Does this answer your question? [Bind to SelectedItems from DataGrid or ListBox in MVVM](https://stackoverflow.com/questions/9880589/bind-to-selecteditems-from-datagrid-or-listbox-in-mvvm) – StayOnTarget Jul 20 '20 at 16:59

1 Answers1

30

You can create a Behavior that synchronizes ListBox.SelectedItems with a collection in your ViewModel:

public class MultiSelectionBehavior : Behavior<ListBox>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        if (SelectedItems != null)
        {
            AssociatedObject.SelectedItems.Clear();
            foreach (var item in SelectedItems)
            {
                AssociatedObject.SelectedItems.Add(item);
            }
        }
    }

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

    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MultiSelectionBehavior), new UIPropertyMetadata(null, SelectedItemsChanged));

    private static void SelectedItemsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var behavior = o as MultiSelectionBehavior;
        if (behavior == null)
            return;

        var oldValue = e.OldValue as INotifyCollectionChanged;
        var newValue = e.NewValue as INotifyCollectionChanged;

        if (oldValue != null)
        {
            oldValue.CollectionChanged -= behavior.SourceCollectionChanged;
            behavior.AssociatedObject.SelectionChanged -= behavior.ListBoxSelectionChanged;
        }
        if (newValue != null)
        {
            behavior.AssociatedObject.SelectedItems.Clear();
            foreach (var item in (IEnumerable)newValue)
            {
                behavior.AssociatedObject.SelectedItems.Add(item);
            }

            behavior.AssociatedObject.SelectionChanged += behavior.ListBoxSelectionChanged;
            newValue.CollectionChanged += behavior.SourceCollectionChanged;
        }
    }

    private bool _isUpdatingTarget;
    private bool _isUpdatingSource;

    void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (_isUpdatingSource)
            return;

        try
        {
            _isUpdatingTarget = true;

            if (e.OldItems != null)
            {
                foreach (var item in e.OldItems)
                {
                    AssociatedObject.SelectedItems.Remove(item);
                }
            }

            if (e.NewItems != null)
            {
                foreach (var item in e.NewItems)
                {
                    AssociatedObject.SelectedItems.Add(item);
                }
            }

            if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                AssociatedObject.SelectedItems.Clear();
            }
        }
        finally
        {
            _isUpdatingTarget = false;
        }
    }

    private void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (_isUpdatingTarget)
            return;

        var selectedItems = this.SelectedItems;
        if (selectedItems == null)
            return;

        try
        {
            _isUpdatingSource = true;

            foreach (var item in e.RemovedItems)
            {
                selectedItems.Remove(item);
            }

            foreach (var item in e.AddedItems)
            {
                selectedItems.Add(item);
            }
        }
        finally
        {
            _isUpdatingSource = false;
        }
    }

}

This behavior can be used as shown below:

        <ListBox ItemsSource="{Binding Items}"
                 DisplayMemberPath="Name"
                 SelectionMode="Extended">
            <i:Interaction.Behaviors>
                <local:MultiSelectionBehavior SelectedItems="{Binding SelectedItems}" />
            </i:Interaction.Behaviors>
        </ListBox>

(note that the SelectedItems collection in your ViewModel has to be initialized; the behavior won't set it, it will only change its content)

xhafan
  • 2,140
  • 1
  • 26
  • 26
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • 8
    To make this work, it should be noted that you have to track down a copy of System.Windows.Interactivity.dll (it's in the Expression Blend SDK) and you have to define the "i:" prefix in the XAML root element, as xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"... you also have to define xmlns:local to point to the namespace that contains MultiSelectionBehavior. – Qwertie Nov 22 '11 at 20:29
  • 2
    Oddly enough, although this class is derived from Behavior, I was able to attach it to a ListView without modification! It turns out that a ListView is derived from ListBox. – Qwertie Nov 22 '11 at 20:30
  • 4
    [PRISM MVVM Reference Implementation](http://msdn.microsoft.com/en-us/library/gg405492%28v=pandp.40%29.aspx) uses this approach too. It has a behaviour called SynchronizeSelectedItems, used in Prism4\MVVM RI\MVVM.Client\Views\MultipleSelectionView.xaml, which synchronizes checked items with the ViewModel property named Selections – surfen Dec 03 '11 at 17:42
  • 1
    @Qwertie please mark this great answer as the answer. Thanks Thomas – Kcvin Jul 15 '16 at 15:41
  • 3
    Got it working in my app. The trick: I had to use an `ObservableCollection` for the `SelectedItems` in my view model, otherwise it kept having a count of 0. – Alex Jul 15 '16 at 16:56
  • 1
    Had to add 'if (e.Action == NotifyCollectionChangedAction.Reset) AssociatedObject.SelectedItems.Clear();' into SourceCollectionChanged() method if you are clearing the target collection programatically. – xhafan Nov 30 '16 at 11:41
  • 1
    doesn't work, it loads correctly with all already selected items right, but once I add or remove a selection the target collection ends up null – kenny Dec 22 '18 at 15:23
  • Works but throws a design time NullRef Exception in SelectedItemsChanged for me. All my properties are initialized. I had to add Null checks for `behavior.AssociatedObject ` to fix it – PandaWood Jun 29 '21 at 09:08
  • I'm getting loads of InvalidCastExceptions from this code, happening so often, it actually slowed down the code significantly - in the `ListBoxSelectionChanged` handler. I believe it's because my DataGrid has combo boxes inside cells (judging by the values when exceptions are happening) and it's going ballistic dealing with these. I had to write an exception handler to catch the bogus errors to do nothing but it happens so often, it practically kills the grid – PandaWood Apr 18 '22 at 03:15