Introduction
This article shows how to manage multiple selection in a View Model using Attached Behaviors.
Why
I was into refactoring some WPF code (of a Metro application) into MVVM design pattern that has heavily used code behind. I stumbled upon a requirement to bind to
the SelectedItems
property of a GridView
. There was a list view in
snapped mode and grid view in other modes (full/fill).
Requirement
- To define
SelectedItems
in ViewModel
- Bind
GridView.ItemsSource
, ListView.ItemsSource
to
Items
- Somehow bind
SelectedItems
of GridView
and ListView
to
SelectedItems
- After step 3, hoping that any selection change in
GridView
should be reflected to
ListView
and vice versa.
Challenges
SelectedItems
property is read only, and cannot be set directly.
SelectedItems
cannot be used for binding expressions, hence cannot be retrieved in View Model.
- WinRT has no support for Behaviors. (For unknown reasons, I wanted to use Attached Behavior.) Thankfully there exists WinRTBehaviors on
CodePlex.
Note - Behaviors are not supported in WinRT natively. WinRTBehaviors is an
Open Source
for providing behavior support. This library is excellent, and provides a behavior extension, exactly similar to
the WPF framework.
Attached Behavior outline
- Behavior name -> MultiSelectBehavior. It will target
ListViewBase
(why -> Base class that provides
multiple selection mechanism).
- Add the
SelectedItems
Dependency Property to Behavior. This property will track selected items of
the associated UI Element (ListView
derived class
is referred as UI Element from hereafter).
- Hookup the
SelectionChanged
event of the UI element in the
OnAttached
, OnDetached
events of the Behavior. In the
OnSelectionChanged
event, sync up the changes
to SelectedItems
(of Behavior). It will propagate UI selection changes to
SelectedItems
in MultiBehavior.
- In the
PropertyChanged
callback of SelectedItems
(in Behavior), listen to
CollectionChanged
of the bound object. Propagate changes in
the CollectionChanged
event to the UI Element.
- Add Behavior to UI elements in XAML.
- Define data binding from
SelectedItems
(in Behavior) to
SelectedItems
in view model.
Code walkthrough
ListView
, GridView
are inherited from ListViewBase
. ListViewBase
provides multiple selection mechanism (SelectedItems
,
SelectionMode
properties).
The MultiSelectBehavior
class is defined targeting ListViewBase
.
public class MultiSelectBehavior : Behavior<ListViewBase>
The SelectedItems
dependency property is created in MultiSelectBehavior
class. It internally holds all the selected items in the ListViewBase
derived class.
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(ObservableCollection<object>),
typeof(MultiSelectBehavior),
new PropertyMetadata(new ObservableCollection<object>(), PropertyChangedCallback));
public ObservableCollection<object> SelectedItems
{
get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
The SelectionChanged
event is hooked up in the Behavior.
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += OnSelectionChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= OnSelectionChanged;
}
When SelectionChanged
is triggered on an element, SelectedItems
is populated.
The _selectionChangedInProgress
flag indicates the selection change is in process.
If this flag is set, no further handling is done (as it would trigger to infinite loop and stackoverflow exception).
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_selectionChangedInProgress) return;
_selectionChangedInProgress = true;
foreach (var item in e.RemovedItems)
{
if (SelectedItems.Contains(item))
{
SelectedItems.Remove(item);
}
}
foreach (var item in e.AddedItems)
{
if (!SelectedItems.Contains(item))
{
SelectedItems.Add(item);
}
}
_selectionChangedInProgress = false;
}
Hook the collection change event of the bound ObservableCollection
. Propagate any change to
the ListView base derived UI elements.
This is done in the PropertyChangedCallback
handler.
private static void PropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
NotifyCollectionChangedEventHandler handler = (s, e) => SelectedItemsChanged(sender, e);
if (args.OldValue is ObservableCollection<object>)
{
(args.OldValue as ObservableCollection<object>).CollectionChanged -= handler;
}
if (args.NewValue is ObservableCollection<object>)
{
(args.NewValue as ObservableCollection<object>).CollectionChanged += handler;
}
}
private static void SelectedItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (sender is MultiSelectBehavior)
{
var listViewBase = (sender as MultiSelectBehavior).AssociatedObject;
var listSelectedItems = listViewBase.SelectedItems;
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
if (listSelectedItems.Contains(item))
{
listSelectedItems.Remove(item);
}
}
}
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (!listSelectedItems.Contains(item))
{
listSelectedItems.Add(item);
}
}
}
}
}
MultiSelectBehavior
is now completed.
Apply the behavior to the UI elements.
Import the namespaces in XAML ( i -> behavior framework library, custom ->
MultiSelectBehavior
class):
xmlns:i ="using:WinRtBehaviors"
xmlns:custom="using:WinRtExt.Behavior"
Add behavior, and attach bound SelectedItems
of behavior to
SelectedItems
of ViewModel:
<i:Interaction.Behaviors>
<custom:MultiSelectBehavior SelectedItems="{Binding SelectedItems, Mode=TwoWay}">
</custom:MultiSelectBehavior>
</i:Interaction.Behaviors>
Following XAML is for two controls (one ListView
, the other
GridView
):
<GridView SelectionMode="Multiple" ItemsSource="{Binding Items}"
BorderBrush="White" BorderThickness="2"
ItemTemplate="{StaticResource textBlockDataTemplate}">
<i:Interaction.Behaviors>
<custom:MultiSelectBehavior SelectedItems="{Binding SelectedItems, Mode=TwoWay}">
</custom:MultiSelectBehavior>
</i:Interaction.Behaviors>
</GridView>
<Rectangle Width="20"></Rectangle>
<ListView SelectionMode="Multiple" ItemsSource="{Binding Items}"
BorderBrush="White" BorderThickness="2"
ItemTemplate="{StaticResource textBlockDataTemplate}">
<i:Interaction.Behaviors>
<custom:MultiSelectBehavior SelectedItems="{Binding SelectedItems, Mode=TwoWay}">
</custom:MultiSelectBehavior>
</i:Interaction.Behaviors>
</ListView>
Both list view and grid view are multi select enabled and in sync. Any selection change in any control is propagated to
the other control.
The code is written in VS2012 RC in Win 8 Release preview. (It is incompatible with older versions, Win 8 Consumer preview,
Win 8 developer preview, and may get broken in future versions.)
To use the code, copy MultiSelectBehavior.cs.
To execute the app, open in VS2012RC in Win 8 release preview. Build the app, deploy app, and launch the app.
Points of Interest
Kudos to WinRTBehaviors that made me mange
multi-selection from View Model and share the information in the form of an article.
History
This is the first version of the article/source code.