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.
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)