Make CollectionView SelectedItem and SelectedItems binding function correctly (#6085)
authorE.Z. Hart <hartez@users.noreply.github.com>
Tue, 28 May 2019 17:54:09 +0000 (11:54 -0600)
committerSamantha Houts <samhouts@users.noreply.github.com>
Tue, 28 May 2019 17:54:09 +0000 (10:54 -0700)
* Add automated test for CollectionView single selection bound item

* Make SelectedItem Two-Way

* Multiple selection test page

* Bindable SelectedItems implementation

* Add automated test

* Simplify null checks

* Add Preserve attribute so linker doesn't break test

* Make multi-item select test smaller so it passes UITests on smaller screens

* Clearer list-to-string method

* Clear native selection on iOS when SelectedItem set to null

fixes #6158
fixes #5832

15 files changed:
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewBoundMultiSelection.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewBoundSingleSelection.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/BoundSelectionModel.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/MultipleBoundSelection.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/MultipleBoundSelection.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SelectionGallery.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SelectionHelpers.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SelectionModeGallery.xaml.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SingleBoundSelection.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SingleBoundSelection.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj
Xamarin.Forms.Core/Items/SelectableItemsView.cs
Xamarin.Forms.Core/Items/SelectionList.cs
Xamarin.Forms.Platform.iOS/CollectionView/SelectableItemsViewController.cs

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewBoundMultiSelection.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewBoundMultiSelection.cs
new file mode 100644 (file)
index 0000000..815befa
--- /dev/null
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+
+#if UITEST
+using Xamarin.Forms.Core.UITests;
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+       [Category(UITestCategories.CollectionView)]
+#endif
+       [Preserve(AllMembers = true)]
+       [Issue(IssueTracker.None, 47803, "CollectionView: Multi Selection Binding", PlatformAffected.All)]
+       public class CollectionViewBoundMultiSelection : TestNavigationPage
+       {
+               protected override void Init()
+               {
+#if APP
+                       Device.SetFlags(new List<string>(Device.Flags ?? new List<string>()) { "CollectionView_Experimental" });
+
+                       PushAsync(new GalleryPages.CollectionViewGalleries.SelectionGalleries.MultipleBoundSelection());
+#endif
+               }
+
+#if UITEST
+               [Test]
+               public void ItemsFromViewModelShouldBeSelected()
+               {
+                       // Initially Items 1 and 2 should be selected (from the view model)
+                       RunningApp.WaitForElement("Selected: Item 1, Item 2");
+
+                       // Tapping Item 3 should select it and updating the binding
+                       RunningApp.Tap("Item 3");       
+                       RunningApp.WaitForElement("Selected: Item 1, Item 2, Item 3");
+
+                       // Test clearing the selection from the view model and updating it
+                       RunningApp.Tap("ClearAndAdd");  
+                       RunningApp.WaitForElement("Selected: Item 1, Item 2");
+
+                       // Test removing an item from the selection
+                       RunningApp.Tap("Item 2");
+                       RunningApp.WaitForElement("Selected: Item 1");
+
+                       // Test setting a new selection list in the view mdoel 
+                       RunningApp.Tap("Reset");        
+                       RunningApp.WaitForElement("Selected: Item 1, Item 2");
+
+                       RunningApp.Tap("Item 0");
+                       
+                       // Test setting the selection directly with CollectionView.SelectedItems 
+                       RunningApp.Tap("DirectUpdate"); 
+                       RunningApp.WaitForElement("Selected: Item 0, Item 3");
+               }
+#endif
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewBoundSingleSelection.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewBoundSingleSelection.cs
new file mode 100644 (file)
index 0000000..cb782e0
--- /dev/null
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+
+#if UITEST
+using Xamarin.Forms.Core.UITests;
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+       [Category(UITestCategories.CollectionView)]
+#endif
+       [Preserve(AllMembers = true)]
+       [Issue(IssueTracker.None, 4539134, "CollectionView: Single Selection Binding", PlatformAffected.All)]
+       public class CollectionViewBoundSingleSelection : TestNavigationPage
+       {
+               protected override void Init()
+               {
+#if APP
+                       Device.SetFlags(new List<string>(Device.Flags ?? new List<string>()) { "CollectionView_Experimental" });
+
+                       PushAsync(new GalleryPages.CollectionViewGalleries.SelectionGalleries.SingleBoundSelection());
+#endif
+               }
+
+#if UITEST
+               [Test]
+               public void SelectionShouldUpdateBinding()
+               {
+                       // Initially Item 2 should be selected (from the view model)
+                       RunningApp.WaitForElement("Selected: Item: 2");
+
+                       // Tapping Item 3 should select it and updating the binding
+                       RunningApp.Tap("Item 3");       
+                       RunningApp.WaitForElement("Selected: Item: 3");
+               }
+#endif
+       }
+}
index c5a8752..bdd2727 100644 (file)
@@ -14,6 +14,8 @@
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla59172.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)FlagTestHelpers.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue5766.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)CollectionViewBoundMultiSelection.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)CollectionViewBoundSingleSelection.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue4684.xaml.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue4992.xaml.cs">
       <DependentUpon>Issue4992.xaml</DependentUpon>
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/BoundSelectionModel.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/BoundSelectionModel.cs
new file mode 100644 (file)
index 0000000..838103c
--- /dev/null
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries
+{
+       [Preserve(AllMembers = true)]
+       internal class BoundSelectionModel : INotifyPropertyChanged
+       {
+               private CollectionViewGalleryTestItem _selectedItem;
+               private ObservableCollection<CollectionViewGalleryTestItem> _items;
+               private ObservableCollection<object> _selectedItems;
+
+               public event PropertyChangedEventHandler PropertyChanged;
+
+               public BoundSelectionModel()
+               {
+                       Items = new ObservableCollection<CollectionViewGalleryTestItem>();
+
+                       for (int n = 0; n < 4; n++)
+                       {
+                               Items.Add(new CollectionViewGalleryTestItem(DateTime.Now.AddDays(n), $"Item {n}", "coffee.png", n));
+                       }
+
+                       SelectedItem = Items[2];
+
+                       SelectedItems = new ObservableCollection<object>()
+                       {
+                               Items[1], Items[2]
+                       };
+               }
+
+               private void SelectedItemsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+               {
+                       OnPropertyChanged(nameof(SelectedItemsText));
+               }
+
+               void OnPropertyChanged([CallerMemberName] string propertyName = null)
+               {
+                       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+               }
+
+               public CollectionViewGalleryTestItem SelectedItem
+               {
+                       get => _selectedItem;
+                       set
+                       {
+                               _selectedItem = value;
+                               OnPropertyChanged();
+                       }
+               }
+
+               public ObservableCollection<object> SelectedItems
+               {
+                       get => _selectedItems;
+                       set
+                       {
+                               if (_selectedItems != null)
+                               {
+                                       _selectedItems.CollectionChanged -= SelectedItemsCollectionChanged;
+                               }
+
+                               _selectedItems = value;
+
+                               _selectedItems.CollectionChanged += SelectedItemsCollectionChanged;
+
+                               OnPropertyChanged();
+                               OnPropertyChanged(nameof(SelectedItemsText));
+                       }
+               }
+
+               public ObservableCollection<CollectionViewGalleryTestItem> Items
+               {
+                       get => _items;
+                       set { _items = value; OnPropertyChanged(); }
+               }
+
+               public string SelectedItemsText => SelectedItems.ToCommaSeparatedList();
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/MultipleBoundSelection.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/MultipleBoundSelection.xaml
new file mode 100644 (file)
index 0000000..d78cf95
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries.MultipleBoundSelection">
+    <ContentPage.Content>
+        <StackLayout Spacing="2">
+
+            <Label Text="The selected items in the CollectionView should always match the 'Selected' Label below. If it does not, this test has failed."
+                FontSize="10" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
+
+            <Label Text="{Binding SelectedItemsText, StringFormat='{}Selected: {0}'}" FontAttributes="Bold"
+                FontSize="10" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" />
+
+            <Button AutomationId="ClearAndAdd" HeightRequest="35" FontSize="10" Text="Clear VM selection and add Items 1 and 2" Clicked="ClearAndAdd" />
+
+            <Button AutomationId="Reset" HeightRequest="35" FontSize="10" Text="Set VM selection to new list" Clicked="ResetClicked" />
+
+            <Button AutomationId="DirectUpdate" HeightRequest="35" FontSize="10" Text="Clear CV selection and add Items 0 and 3" Clicked="DirectUpdateClicked" />
+
+            <CollectionView x:Name="CollectionView" ItemsSource="{Binding Items}" 
+                            SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}">
+                <CollectionView.ItemTemplate>
+                    <DataTemplate>
+                        <StackLayout>
+                            <Image Source="{Binding Image}" HeightRequest="30" />
+                            <Label FontSize="10" Text="{Binding Caption}"></Label>
+                        </StackLayout>
+                    </DataTemplate>
+                </CollectionView.ItemTemplate>
+            </CollectionView>
+
+        </StackLayout>
+    </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/MultipleBoundSelection.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/MultipleBoundSelection.xaml.cs
new file mode 100644 (file)
index 0000000..955f68e
--- /dev/null
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.ObjectModel;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class MultipleBoundSelection : ContentPage
+       {
+               BoundSelectionModel _vm;
+
+               public MultipleBoundSelection()
+               {
+                       _vm = new BoundSelectionModel();
+                       BindingContext = _vm;
+                       InitializeComponent();
+               }
+
+               private void ClearAndAdd(object sender, EventArgs e)
+               {
+                       _vm.SelectedItems.Clear();
+                       _vm.SelectedItems.Add(_vm.Items[1]);
+                       _vm.SelectedItems.Add(_vm.Items[2]);
+               }
+
+               private void ResetClicked(object sender, EventArgs e)
+               {
+                       _vm.SelectedItems = new ObservableCollection<object>
+                       {
+                               _vm.Items[1],
+                               _vm.Items[2]
+                       };
+               }
+
+               private void DirectUpdateClicked(object sender, EventArgs e)
+               {
+                       CollectionView.SelectedItems.Clear();
+                       CollectionView.SelectedItems.Add(_vm.Items[0]);
+                       CollectionView.SelectedItems.Add(_vm.Items[3]);
+               }
+       }
+}
\ No newline at end of file
index a4c1517..6a10d5f 100644 (file)
                                                        new PreselectedItemGallery(), Navigation),
                                                GalleryBuilder.NavButton("Preselected Items", () =>
                                                        new PreselectedItemsGallery(), Navigation),
+                                               GalleryBuilder.NavButton("Single Selection, Bound", () =>
+                                                       new SingleBoundSelection(), Navigation),
+                                               GalleryBuilder.NavButton("Multiple Selection, Bound", () =>
+                                                       new MultipleBoundSelection(), Navigation),
                                        }
                                }
                        };
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SelectionHelpers.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SelectionHelpers.cs
new file mode 100644 (file)
index 0000000..9042c18
--- /dev/null
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries
+{
+       internal static class SelectionHelpers
+       {
+               public static string ToCommaSeparatedList(this IEnumerable<object> items)
+               {
+                       if (items == null)
+                       {
+                               return string.Empty;
+                       }
+
+                       return string.Join(", ", items.Cast<CollectionViewGalleryTestItem>().Select(i => i.Caption));
+               }
+       }
+}
\ No newline at end of file
index 8363e67..12c5f6e 100644 (file)
@@ -35,8 +35,8 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionG
 
                void UpdateSelectionInfo(IEnumerable<object> currentSelectedItems, IEnumerable<object> previousSelectedItems)
                {
-                       var previous = ToList(previousSelectedItems);
-                       var current = ToList(currentSelectedItems);
+                       var previous = previousSelectedItems.ToCommaSeparatedList();
+                       var current = currentSelectedItems.ToCommaSeparatedList();
 
                        if (string.IsNullOrEmpty(previous))
                        {
@@ -58,7 +58,7 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionG
 
                        if(CollectionView.SelectionMode == SelectionMode.Multiple)
                        {
-                               current = ToList(CollectionView?.SelectedItems);
+                               current = CollectionView?.SelectedItems.ToCommaSeparatedList();
                        }
                        else if (CollectionView.SelectionMode == SelectionMode.Single)
                        {
@@ -67,16 +67,5 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionG
 
                        SelectedItemsCommand.Text = $"Selection (command): {current}";
                }
-
-               static string ToList(IEnumerable<object> items)
-               {
-                       if (items == null)
-                       {
-                               return string.Empty;
-                       }
-
-                       return items.Aggregate(string.Empty, 
-                               (s, o) => s + (s.Length == 0 ? "" : ", ") + ((CollectionViewGalleryTestItem)o).Caption);
-               }
        }
 }
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SingleBoundSelection.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SingleBoundSelection.xaml
new file mode 100644 (file)
index 0000000..70e561f
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries.SingleBoundSelection">
+    <ContentPage.Content>
+        <StackLayout Spacing="5">
+
+            <Label Text="The selected item in the CollectionView should match the 'Selected' Label below. If it does not, this test has failed."
+                VerticalOptions="CenterAndExpand" 
+                HorizontalOptions="CenterAndExpand" />
+
+            <Label Text="{Binding SelectedItem, StringFormat='{}Selected: {0}'}"
+                VerticalOptions="CenterAndExpand" 
+                HorizontalOptions="CenterAndExpand" />
+
+            <Button AutomationId="Reset" Text="Reset Selection to Item 0" Clicked="ResetClicked" />
+            
+            <Button AutomationId="Clear" Text="Clear Selection" Clicked="ClearClicked" />
+
+            <CollectionView ItemsSource="{Binding Items}" SelectionMode="Single" SelectedItem="{Binding SelectedItem}">
+                <CollectionView.ItemTemplate>
+                    <DataTemplate>
+                        <StackLayout>
+                            <Image Source="{Binding Image}" HeightRequest="50" />
+                            <Label Text="{Binding Caption}"></Label>
+                        </StackLayout>
+                    </DataTemplate>
+                </CollectionView.ItemTemplate>
+            </CollectionView>
+
+        </StackLayout>
+    </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SingleBoundSelection.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/SelectionGalleries/SingleBoundSelection.xaml.cs
new file mode 100644 (file)
index 0000000..c106e04
--- /dev/null
@@ -0,0 +1,28 @@
+using System;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class SingleBoundSelection : ContentPage
+       {
+               BoundSelectionModel _vm;
+
+               public SingleBoundSelection()
+               {
+                       InitializeComponent();
+                       _vm = new BoundSelectionModel();
+                       BindingContext = _vm;
+               }
+
+               private void ResetClicked(object sender, EventArgs e)
+               {
+                       _vm.SelectedItem = _vm.Items[0];
+               }
+
+               private void ClearClicked(object sender, EventArgs e)
+               {
+                       _vm.SelectedItem = null;
+               }
+       }
+}
\ No newline at end of file
index 02bb02c..5ef591f 100644 (file)
@@ -56,6 +56,9 @@
     <EmbeddedResource Update="GalleryPages\CollectionViewGalleries\EmptyViewGalleries\EmptyViewSwapGallery.xaml">
        <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
     </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\CollectionViewGalleries\SelectionGalleries\MultipleBoundSelection.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
     <EmbeddedResource Update="GalleryPages\CollectionViewGalleries\SelectionGalleries\PreselectedItemsGallery.xaml">
        <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
     </EmbeddedResource>
index 0faeb05..72b76e6 100644 (file)
@@ -12,12 +12,15 @@ namespace Xamarin.Forms
 
                public static readonly BindableProperty SelectedItemProperty =
                        BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(SelectableItemsView), default(object),
+                               defaultBindingMode: BindingMode.TwoWay,
                                propertyChanged: SelectedItemPropertyChanged);
 
-               static readonly BindablePropertyKey SelectedItemsPropertyKey =
-                       BindableProperty.CreateReadOnly(nameof(SelectedItems), typeof(IList<object>), typeof(SelectableItemsView), null);
-
-               public static readonly BindableProperty SelectedItemsProperty = SelectedItemsPropertyKey.BindableProperty;
+               public static readonly BindableProperty SelectedItemsProperty =
+                       BindableProperty.Create(nameof(SelectedItems), typeof(IList<object>), typeof(SelectableItemsView), null,
+                               defaultBindingMode: BindingMode.OneWay,
+                               propertyChanged: SelectedItemsPropertyChanged,
+                               coerceValue: CoerceSelectedItems,
+                               defaultValueCreator: DefaultValueCreator);
 
                public static readonly BindableProperty SelectionChangedCommandProperty =
                        BindableProperty.Create(nameof(SelectionChangedCommand), typeof(ICommand), typeof(SelectableItemsView));
@@ -26,10 +29,10 @@ namespace Xamarin.Forms
                        BindableProperty.Create(nameof(SelectionChangedCommandParameter), typeof(object),
                                typeof(SelectableItemsView));
 
+               static readonly IList<object> s_empty = new List<object>(0);
+
                public SelectableItemsView()
                {
-                       var selectionList = new SelectionList(this);
-                       SetValue(SelectedItemsPropertyKey, selectionList);
                }
 
                public object SelectedItem
@@ -41,6 +44,7 @@ namespace Xamarin.Forms
                public IList<object> SelectedItems
                {
                        get => (IList<object>)GetValue(SelectedItemsProperty);
+                       set => SetValue(SelectedItemsProperty, new SelectionList(this, value));
                }
 
                public ICommand SelectionChangedCommand
@@ -67,9 +71,39 @@ namespace Xamarin.Forms
                {
                }
 
+               static object CoerceSelectedItems(BindableObject bindable, object value)
+               {
+                       if (value == null)
+                       {
+                               return new SelectionList((SelectableItemsView)bindable);
+                       }
+
+                       if(value is SelectionList)
+                       {
+                               return value;
+                       }
+
+                       return new SelectionList((SelectableItemsView)bindable, value as IList<object>);
+               }
+
+               static object DefaultValueCreator(BindableObject bindable)
+               {
+                       return new SelectionList((SelectableItemsView)bindable);
+               }
+
+               static void SelectedItemsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+               {
+                       var selectableItemsView = (SelectableItemsView)bindable;
+                       var oldSelection = (IList<object>)oldValue ?? s_empty;
+                       var newSelection = (IList<object>)newValue ?? s_empty;
+
+                       selectableItemsView.SelectedItemsPropertyChanged(oldSelection, newSelection);
+               }
+
                internal void SelectedItemsPropertyChanged(IList<object> oldSelection, IList<object> newSelection)
                {
                        SelectionPropertyChanged(this, new SelectionChangedEventArgs(oldSelection, newSelection));
+                       
                        OnPropertyChanged(SelectedItemsProperty.PropertyName);
                }
 
index 467594d..5bfe0ea 100644 (file)
@@ -1,42 +1,55 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 
 namespace Xamarin.Forms
 {
        // Used by the SelectableItemsView to keep track of (and respond to changes in) the SelectedItems property
        internal class SelectionList : IList<object>
        {
-               readonly SelectableItemsView _selectableItemsView;
-               List<object> _internal;
                static readonly IList<object> s_empty = new List<object>(0);
+               readonly SelectableItemsView _selectableItemsView;
+               readonly IList<object> _internal;
+               IList<object> _shadow;
+               bool _externalChange;
 
-               public SelectionList(SelectableItemsView selectableItemsView)
+               public SelectionList(SelectableItemsView selectableItemsView, IList<object> items = null)
                {
                        _selectableItemsView = selectableItemsView ?? throw new ArgumentNullException(nameof(selectableItemsView));
-                       _internal = new List<object>();
+                       _internal = items ?? new List<object>();
+                       _shadow = Copy();
+
+                       if (items is INotifyCollectionChanged incc)
+                       {
+                               incc.CollectionChanged += OnCollectionChanged;
+                       }
                }
 
                public object this[int index] { get => _internal[index]; set => _internal[index] = value; }
 
                public int Count => _internal.Count;
+
                public bool IsReadOnly => false;
 
                public void Add(object item)
                {
-                       var oldItems = Copy();
-
+                       _externalChange = true;
                        _internal.Add(item);
+                       _externalChange = false;
 
-                       _selectableItemsView.SelectedItemsPropertyChanged(oldItems, Copy());
+                       _selectableItemsView.SelectedItemsPropertyChanged(_shadow, _internal);
+                       _shadow.Add(item);
                }
 
                public void Clear()
                {
-                       var oldItems = Copy();
+                       _externalChange = true;
                        _internal.Clear();
+                       _externalChange = false;
 
-                       _selectableItemsView.SelectedItemsPropertyChanged(oldItems, s_empty);
+                       _selectableItemsView.SelectedItemsPropertyChanged(_shadow, s_empty);
+                       _shadow.Clear();
                }
 
                public bool Contains(object item)
@@ -61,22 +74,24 @@ namespace Xamarin.Forms
 
                public void Insert(int index, object item)
                {
-                       var oldItems = Copy();
-
+                       _externalChange = true;
                        _internal.Insert(index, item);
+                       _externalChange = false;
 
-                       _selectableItemsView.SelectedItemsPropertyChanged(oldItems, Copy());
+                       _selectableItemsView.SelectedItemsPropertyChanged(_shadow, _internal);
+                       _shadow.Insert(index, item);
                }
 
                public bool Remove(object item)
                {
-                       var oldItems = Copy();
-
+                       _externalChange = true;
                        var removed = _internal.Remove(item);
+                       _externalChange = false;
 
                        if (removed)
                        {
-                               _selectableItemsView.SelectedItemsPropertyChanged(oldItems, Copy());
+                               _selectableItemsView.SelectedItemsPropertyChanged(_shadow, _internal);
+                               _shadow.Remove(item);
                        }
 
                        return removed;
@@ -84,11 +99,12 @@ namespace Xamarin.Forms
 
                public void RemoveAt(int index)
                {
-                       var oldItems = Copy();
-
+                       _externalChange = true;
                        _internal.RemoveAt(index);
+                       _externalChange = false;
 
-                       _selectableItemsView.SelectedItemsPropertyChanged(oldItems, Copy());
+                       _selectableItemsView.SelectedItemsPropertyChanged(_shadow, _internal);
+                       _shadow.RemoveAt(index);
                }
 
                IEnumerator IEnumerable.GetEnumerator()
@@ -107,14 +123,19 @@ namespace Xamarin.Forms
                        return items;
                }
 
-               public void ClearQuietly()
+               void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
                {
-                       _internal.Clear();
-               }
+                       if (_externalChange)
+                       {
+                               // If this change was initiated by a renderer or direct manipulation of ColllectionView.SelectedItems,
+                               // we don't need to send a selection change notification
+                               return;
+                       }
 
-               public void AddQuietly(object item)
-               {
-                       _internal.Add(item);
+                       // This change is coming from a bound viewmodel property
+                       // Emit a selection change notification, then bring the shadow copy up-to-date
+                       _selectableItemsView.SelectedItemsPropertyChanged(_shadow, _internal);
+                       _shadow = Copy();
                }
        }
 }
index 73fb7f0..ae4e6ee 100644 (file)
@@ -34,6 +34,17 @@ namespace Xamarin.Forms.Platform.iOS
                        CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
                }
 
+               // Called by Forms to clear the native selection
+               internal void ClearSelection()
+               {
+                       var selectedItemIndexes = CollectionView.GetIndexPathsForSelectedItems();
+
+                       foreach (var index in selectedItemIndexes)
+                       {
+                               CollectionView.DeselectItem(index, true);
+                       }
+               }
+
                void FormsSelectItem(NSIndexPath indexPath)
                {
                        var mode = SelectableItemsView.SelectionMode;
@@ -87,6 +98,11 @@ namespace Xamarin.Forms.Platform.iOS
                                        {
                                                SelectItem(selectedItem);
                                        }
+                                       else
+                                       {
+                                               // SelectedItem has been set to null; if an item is selected, we need to de-select it
+                                               ClearSelection();
+                                       }
                                
                                        return;
                                case SelectionMode.Multiple: