I tried to find a way to bind GridView’s SelectedItems property to a ViewModel’s property. As we know, the property is read only so there is no DependencyProperty for it. Finding a workaround which made it possible to bind to it turned out to be a quite difficult task.
Using Attached Properties (didn’t work)
My first approach was creating a class called GridViewHelper which contained attached property which would have helped me bind to the SelectedItems property. The problem I had was that when the owner type of the property was GridViewHelper, my breakpoint in property changed callback never fired. And when the owner type was just GridView, the breakpoint fired but as a result I got the actual Binding object instead of the results of the binding, which would have been some objects from my ViewModel.
So this was my first approach which didn’t work:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class GridViewHelper { public static object GetSelectedItems(DependencyObject obj) { return (object)obj.GetValue(SelectedItemsProperty); } public static void SetSelectedItems(DependencyObject obj, object value) { obj.SetValue(SelectedItemsProperty, value); } // Using a DependencyProperty as the backing store for SelectedItems. This enables animation, styling, binding, etc... public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached("SelectedItems", typeof(object), typeof(GridViewHelper), new PropertyMetadata(null, SelectedItemsChanged)); private static void SelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // get the grid view and set handler for SelectedChanged event } } |
Using WinRtBehaviors library (works!)
I continued searching for a working solution or tips to create a working solution. I stumbled upon CodeProject which seemed to have a great solution using Behaviors. WinRT doesn’t support behaviors natively but awesome people behind WinRtBehaviors library (also available via NuGet) have created behaviors for WinRT.
The solution in CodeProject is good but not perfect, at least not to my needs. I don’t like the fact that I’m forced to use object as the type of ObservableCollection as in my ViewModels everything is typed to their actual types. And because of generic variance issues I can’t cast ObservableCollection<MyType> to ObservableCollection<object>. Also there seemed to be a bug in the solution: when I cleared the collection in my ViewModel the View still had items selected.
So after a bit of tweaking, here is the solution I’m using at the moment.
Base class (from CodeProject with support for Clear() and Type parameter)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
/// <summary> /// Adds Multiple Selection behavior to ListViewBase /// This adds capabilities to set/get Multiple selection from Binding (ViewModel) /// </summary> /// <remarks>http://www.codeproject.com/Articles/412417/Managing-Multiple-selection-in-View-Model-NET-Metr</remarks> public class MultiSelectBehaviorBase<T> : Behavior<ListViewBase> { #region SelectedItems Attached Property public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register( "SelectedItems", typeof(ObservableCollection<T>), typeof(MultiSelectBehaviorBase<T>), new PropertyMetadata(new ObservableCollection<T>(), PropertyChangedCallback)); #endregion #region private private bool _selectionChangedInProgress; // Flag to avoid infinite loop if same viewmodel is shared by multiple controls #endregion public MultiSelectBehaviorBase() { SelectedItems = new ObservableCollection<T>(); } public ObservableCollection<T> SelectedItems { get { return (ObservableCollection<T>)GetValue(SelectedItemsProperty); } set { SetValue(SelectedItemsProperty, value); } } protected override void OnAttached() { base.OnAttached(); AssociatedObject.SelectionChanged += OnSelectionChanged; } protected override void OnDetaching() { base.OnDetaching(); AssociatedObject.SelectionChanged -= OnSelectionChanged; // unhook the collection notifications if wired up too. if (SelectedItems != null) SelectedItems.CollectionChanged -= SelectedItemsChanged; } private static void PropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args) { MultiSelectBehaviorBase<T> msbSender = sender as MultiSelectBehaviorBase<T>; msbSender.WireNewSelectedItems(args.OldValue as ObservableCollection<T>, args.NewValue as ObservableCollection<T>); } private void WireNewSelectedItems(ObservableCollection<T> OldValue, ObservableCollection<T> NewValue) { // NotifyCollectionChangedEventHandler handler = (s, e) => SelectedItemsChanged(sender, e); if (OldValue != null) { OldValue.CollectionChanged -= SelectedItemsChanged; } if (NewValue != null) { var listViewBase = AssociatedObject; if (listViewBase != null) { var listSelectedItems = listViewBase.SelectedItems; bool bOldInProgress = _selectionChangedInProgress; _selectionChangedInProgress = true; // don't notify, as we're in control listSelectedItems.Clear(); foreach (var v in NewValue) { listSelectedItems.Add(v); } _selectionChangedInProgress = bOldInProgress; } // wire up notifications on the new collection NewValue.CollectionChanged += SelectedItemsChanged; ; } } private void SelectedItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { // not needed... use this instead:// if (sender is MultiSelectBehavior) { var listViewBase = /*(sender as MultiSelectBehavior).*/ AssociatedObject; var listSelectedItems = listViewBase.SelectedItems; if (e.Action == NotifyCollectionChangedAction.Reset) listSelectedItems.Clear(); else { 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); } } } } } } private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (_selectionChangedInProgress) return; _selectionChangedInProgress = true; foreach (T item in e.RemovedItems) { if (SelectedItems.Contains(item)) { SelectedItems.Remove(item); } } foreach (T item in e.AddedItems) { if (!SelectedItems.Contains(item)) { SelectedItems.Add(item); } } _selectionChangedInProgress = false; } } |
Examples how to derive it:
|
1 2 3 4 5 6 7 |
public class MyTypeMultiSelectBehavior : MultiSelectBehaviorBase<MyType> { } public class MultiSelectBehavior : MultiSelectBehaviorBase<object> { } |
How to use it in XAML:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
... xmlns:i="using:WinRtBehaviors" ... <GridView x:Name="gridItems" AutomationProperties.AutomationId="ItemsGridView" AutomationProperties.Name="Items" TabIndex="1" Grid.Column="0" Grid.Row="1" Padding="0,0,116,46" ItemsSource="{Binding Path=VM.Items}" ItemTemplate="{StaticResource Standard250x250ItemTemplate}" SelectionMode="Multiple" IsSwipeEnabled="false"> <i:Interaction.Behaviors> <ctrl:MyTypeMultiSelectBehavior SelectedItems="{Binding Path=VM.SelectedItems, Mode=TwoWay}" /> </i:Interaction.Behaviors> </GridView> |



Trackbacks/Pingbacks
[...] Windows 8 JavaScript games, Space Cadet and Catapult Wars, to the leaderboard service…”Binding GridView.SelectedItems property (Janne Rautiola)“I tried to find a way to bind GridView’s SelectedItems property to a [...]