--- /dev/null
+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
--- /dev/null
+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
+ }
+}
<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>
--- /dev/null
+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
--- /dev/null
+<?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
--- /dev/null
+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
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),
}
}
};
--- /dev/null
+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
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))
{
if(CollectionView.SelectionMode == SelectionMode.Multiple)
{
- current = ToList(CollectionView?.SelectedItems);
+ current = CollectionView?.SelectedItems.ToCommaSeparatedList();
}
else if (CollectionView.SelectionMode == SelectionMode.Single)
{
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
--- /dev/null
+<?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
--- /dev/null
+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
<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>
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));
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
public IList<object> SelectedItems
{
get => (IList<object>)GetValue(SelectedItemsProperty);
+ set => SetValue(SelectedItemsProperty, new SelectionList(this, value));
}
public ICommand SelectionChangedCommand
{
}
+ 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);
}
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)
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;
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()
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();
}
}
}
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;
{
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: