From a9b04a5c2c5301248a9e2f6452d5e43cc11cf491 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Aug 2020 21:03:25 +0200 Subject: [PATCH] Started integrating new SelectionModel. --- src/Avalonia.Controls/ListBox.cs | 4 +- .../Primitives/SelectingItemsControl.cs | 947 +++++------------- .../Selection/ISelectionModel.cs | 28 +- .../Selection/SelectionModel.cs | 72 +- .../Utils/SelectedItemsSync.cs | 259 +++++ .../Primitives/SelectingItemsControlTests.cs | 98 +- .../SelectingItemsControlTests_Multiple.cs | 4 +- .../Selection/SelectionModelTests_Multiple.cs | 32 + .../Selection/SelectionModelTests_Single.cs | 120 ++- 9 files changed, 819 insertions(+), 745 deletions(-) create mode 100644 src/Avalonia.Controls/Utils/SelectedItemsSync.cs diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 2162019343..c58c014fc0 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -95,12 +95,12 @@ namespace Avalonia.Controls /// /// Selects all items in the . /// - public void SelectAll() => base.SelectAll(); + public void SelectAll() => Selection.SelectAll(); /// /// Deselects all items in the . /// - public void UnselectAll() => base.UnselectAll(); + public void UnselectAll() => Selection.Clear(); /// protected override IItemContainerGenerator CreateItemContainerGenerator() diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 78cad6f274..df31708c17 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1,19 +1,16 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Linq; -using System.Net.Http.Headers; -using Avalonia.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; -using Avalonia.Logging; using Avalonia.VisualTree; #nullable enable @@ -28,9 +25,9 @@ namespace Avalonia.Controls.Primitives /// provides a base class for s /// that maintain a selection (single or multiple). By default only its /// and properties are visible; the - /// current multiple together with the - /// properties are protected, however a derived class can expose these if it wishes to support - /// multiple selection. + /// current multiple and together with the + /// properties are protected, however a derived class can expose + /// these if it wishes to support multiple selection. /// /// /// maintains a selection respecting the current @@ -79,6 +76,15 @@ namespace Avalonia.Controls.Primitives o => o.SelectedItems, (o, v) => o.SelectedItems = v); + /// + /// Defines the property. + /// + protected static readonly DirectProperty SelectionProperty = + AvaloniaProperty.RegisterDirect( + nameof(Selection), + o => o.Selection, + (o, v) => o.Selection = v); + /// /// Defines the property. /// @@ -105,15 +111,12 @@ namespace Avalonia.Controls.Primitives RoutingStrategies.Bubble); private static readonly IList Empty = Array.Empty(); - private readonly Selection _selection = new Selection(); - private int _selectedIndex = -1; - private object? _selectedItem; - private IList? _selectedItems; + private SelectedItemsSync? _selectedItemsSync; + private ISelectionModel? _selection; + private int _oldSelectedIndex; + private object? _oldSelectedItem; + private int _initializing; private bool _ignoreContainerSelectionChanged; - private bool _syncingSelectedItems; - private int _updateCount; - private int _updateSelectedIndex; - private object? _updateSelectedItem; /// /// Initializes static members of the class. @@ -146,20 +149,8 @@ namespace Avalonia.Controls.Primitives /// public int SelectedIndex { - get => _selectedIndex; - set - { - if (_updateCount == 0) - { - var effective = (value >= 0 && value < ItemCount) ? value : -1; - UpdateSelectedItem(effective); - } - else - { - _updateSelectedIndex = value; - _updateSelectedItem = null; - } - } + get => Selection.SelectedIndex; + set => Selection.SelectedIndex = value; } /// @@ -167,48 +158,51 @@ namespace Avalonia.Controls.Primitives /// public object? SelectedItem { - get => _selectedItem; - set - { - if (_updateCount == 0) - { - UpdateSelectedItem(IndexOf(Items, value)); - } - else - { - _updateSelectedItem = value; - _updateSelectedIndex = int.MinValue; - } - } + get => Selection.SelectedItem; + set => Selection.SelectedItem = value; } /// /// Gets or sets the selected items. /// protected IList SelectedItems + { + get => SelectedItemsSync.GetOrCreateSelectedItems(); + set => SelectedItemsSync.SetSelectedItems(value); + } + + /// + /// Gets or sets the model that holds the current selection. + /// + protected ISelectionModel Selection { get { - if (_selectedItems == null) + if (_selection is null) { - _selectedItems = new AvaloniaList(); - SubscribeToSelectedItems(); + _selection = CreateDefaultSelectionModel(); + InitializeSelectionModel(_selection); } - - return _selectedItems; + + return _selection; } - set { - if (value?.IsFixedSize == true || value?.IsReadOnly == true) + value ??= CreateDefaultSelectionModel(); + + if (_selection != value) { - throw new NotSupportedException( - "Cannot use a fixed size or read-only collection as SelectedItems."); - } + if (value.Source != null && value.Source != Items) + { + throw new ArgumentException( + "The supplied ISelectionModel already has an assigned Source but this " + + "collection is different to the Items on the control."); + } - UnsubscribeFromSelectedItems(); - _selectedItems = value ?? new AvaloniaList(); - SubscribeToSelectedItems(); + DeinitializeSelectionModel(_selection); + _selection = value; + InitializeSelectionModel(_selection); + } } } @@ -230,20 +224,20 @@ namespace Avalonia.Controls.Primitives /// protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; + private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection); + /// public override void BeginInit() { base.BeginInit(); - - InternalBeginInit(); + ++_initializing; } /// public override void EndInit() { - InternalEndInit(); - base.EndInit(); + --_initializing; } /// @@ -286,79 +280,22 @@ namespace Avalonia.Controls.Primitives { base.ItemsChanged(e); - if (_updateCount == 0) - { - var newIndex = -1; - - if (SelectedIndex != -1) - { - newIndex = IndexOf((IEnumerable?)e.NewValue, SelectedItem); - } - - if (AlwaysSelected && Items != null && Items.Cast().Any()) - { - newIndex = 0; - } - - SelectedIndex = newIndex; - } - } - - /// - protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_updateCount > 0) - { - base.ItemsCollectionChanged(sender, e); - return; - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); - break; - case NotifyCollectionChangedAction.Remove: - _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); - break; - } - - base.ItemsCollectionChanged(sender, e); - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - if (AlwaysSelected && SelectedIndex == -1) - { - SelectedIndex = 0; - } - else - { - UpdateSelectedItem(_selection.First(), false); - } - - break; - - case NotifyCollectionChangedAction.Remove: - UpdateSelectedItem(_selection.First(), false); - ResetSelectedItems(); - break; + //if (_updateCount == 0) + //{ + // var newIndex = -1; - case NotifyCollectionChangedAction.Replace: - UpdateSelectedItem(SelectedIndex, false); - ResetSelectedItems(); - break; + // if (SelectedIndex != -1) + // { + // newIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); + // } - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Reset: - SelectedIndex = IndexOf(Items, SelectedItem); + // if (AlwaysSelected && Items != null && Items.Cast().Any()) + // { + // newIndex = 0; + // } - if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) - { - SelectedIndex = 0; - } - break; - } + // SelectedIndex = newIndex; + //} } /// @@ -366,36 +303,18 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); - var resetSelectedItems = false; - foreach (var container in e.Containers) { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectionMode.HasFlag(SelectionMode.Multiple)) - { - if (_selection.Add(container.Index)) - { - resetSelectedItems = true; - } - } - else - { - SelectedIndex = container.Index; - } - + Selection.Select(container.Index); MarkContainerSelected(container.ContainerControl, true); } - else if (_selection.Contains(container.Index)) + else if (Selection.IsSelected(container.Index) == true) { MarkContainerSelected(container.ContainerControl, true); } } - - if (resetSelectedItems) - { - ResetSelectedItems(); - } } /// @@ -424,7 +343,7 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - bool selected = _selection.Contains(i.Index); + bool selected = Selection.IsSelected(i.Index); MarkContainerSelected(i.ContainerControl, selected); } } @@ -435,7 +354,7 @@ namespace Avalonia.Controls.Primitives { base.OnDataContextBeginUpdate(); - InternalBeginInit(); + //InternalBeginInit(); } /// @@ -443,7 +362,17 @@ namespace Avalonia.Controls.Primitives { base.OnDataContextEndUpdate(); - InternalEndInit(); + //InternalEndInit(); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + + if (_selection is object) + { + _selection.Source = Items; + } } protected override void OnKeyDown(KeyEventArgs e) @@ -460,12 +389,22 @@ namespace Avalonia.Controls.Primitives (((SelectionMode & SelectionMode.Multiple) != 0) || (SelectionMode & SelectionMode.Toggle) != 0)) { - SelectAll(); + Selection.SelectAll(); e.Handled = true; } } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ItemsProperty && _initializing == 0 && _selection is object) + { + _selection.Source = change.NewValue.GetValueOrDefault(); + } + } + /// /// Moves the selection in the specified direction relative to the current selection. /// @@ -502,62 +441,6 @@ namespace Avalonia.Controls.Primitives return false; } - /// - /// Selects all items in the control. - /// - protected void SelectAll() - { - _selection.Clear(); - - for (var i = 0; i < ItemCount; ++i) - { - _selection.Add(i); - } - - UpdateSelectedItem(0, false); - - SyncSelection(); - } - - /// - /// Deselects all items in the control. - /// - protected void UnselectAll() => UpdateSelectedItem(-1); - - private void SyncSelection() - { - UpdateSelectedItems(() => - { - var selection = _selection.Select(x => ElementAt(Items, x)).ToList(); - var added = selection.Except(SelectedItems.Cast()).ToList(); - var removed = SelectedItems.Cast().Except(selection).ToList(); - - foreach (var container in ItemContainerGenerator.Containers) - { - MarkItemSelected(container.Index, _selection.Contains(container.Index)); - } - - foreach (var i in added) - { - SelectedItems.Add(i); - } - - foreach (var i in removed) - { - SelectedItems.Remove(i); - } - - if (added.Count > 0 || removed.Count > 0) - { - var changed = new SelectionChangedEventArgs( - SelectionChangedEvent, - removed ?? Empty, - added ?? Empty); - RaiseEvent(changed); - } - }); - } - /// /// Updates the selection for an item based on user interaction. /// @@ -573,77 +456,62 @@ namespace Avalonia.Controls.Primitives bool toggleModifier = false, bool rightButton = false) { - if (index != -1) + if (index < 0 || index >= ItemCount) { - if (select) - { - var mode = SelectionMode; - var multi = (mode & SelectionMode.Multiple) != 0; - var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); - var range = multi && rangeModifier; - - if (rightButton) - { - if (!_selection.Contains(index)) - { - UpdateSelectedItem(index); - } - } - else if (range) - { - var start = SelectedIndex != -1 ? SelectedIndex : 0; - var step = start < index ? 1 : -1; - - _selection.Clear(); - - for (var i = start; i != index; i += step) - { - _selection.Add(i); - } + return; + } - _selection.Add(index); - SyncSelection(); - } - else if (multi && toggle) - { - if (!_selection.Contains(index)) - { - _selection.Add(index); - } - else - { - _selection.Remove(index); - - if (index == _selectedIndex) - { - UpdateSelectedItem(_selection.First(), false); - } - } - - SyncSelection(); - } - else if (toggle) - { - SelectedIndex = (SelectedIndex == index) ? -1 : index; - } - else - { - UpdateSelectedItem(index); - } + var mode = SelectionMode; + var multi = (mode & SelectionMode.Multiple) != 0; + var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); + var range = multi && rangeModifier; - if (Presenter?.Panel != null) - { - var container = ItemContainerGenerator.ContainerFromIndex(index); - KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter.Panel, - container); - } + if (!select) + { + Selection.Deselect(index); + } + else if (rightButton) + { + if (Selection.IsSelected(index) == false) + { + SelectedIndex = index; + } + } + else if (range) + { + using var operation = Selection.BatchUpdate(); + Selection.Clear(); + Selection.SelectRange(Selection.AnchorIndex, index); + } + else if (multi && toggle) + { + if (Selection.IsSelected(index) == true) + { + Selection.Deselect(index); } else { - LostSelection(); + Selection.Select(index); } } + else if (toggle) + { + SelectedIndex = (SelectedIndex == index) ? -1 : index; + } + else + { + using var operation = Selection.BatchUpdate(); + Selection.Clear(); + Selection.Select(index); + } + + if (Presenter?.Panel != null) + { + var container = ItemContainerGenerator.ContainerFromIndex(index); + KeyboardNavigation.SetTabOnceActiveElement( + (InputElement)Presenter.Panel, + container); + } } /// @@ -700,6 +568,73 @@ namespace Avalonia.Controls.Primitives return false; } + /// + /// Called when is raised on + /// . + /// + /// The sender. + /// The event args. + private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem) + { + if (Selection.AnchorIndex > 0) + { + ScrollIntoView(Selection.AnchorIndex); + } + } + else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex)) + { + RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex); + _oldSelectedIndex = SelectedIndex; + } + else if (e.PropertyName == nameof(ISelectionModel.SelectedItem)) + { + RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem); + _oldSelectedItem = SelectedItem; + } + } + + /// + /// Called when event is raised on + /// . + /// + /// The sender. + /// The event args. + private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + void Mark(int index, bool selected) + { + var container = ItemContainerGenerator.ContainerFromIndex(index); + + if (container != null) + { + MarkContainerSelected(container, selected); + } + } + + foreach (var i in e.SelectedIndexes) + { + Mark(i, true); + } + + foreach (var i in e.DeselectedIndexes) + { + Mark(i, false); + } + + var route = BuildEventRoute(SelectionChangedEvent); + + if (route.HasHandlers) + { + var ev = new SelectionChangedEventArgs( + SelectionChangedEvent, + e.DeselectedItems.ToList(), + e.SelectedItems.ToList()); + RaiseEvent(ev); + } + } + /// /// Called when a container raises the . /// @@ -726,23 +661,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Called when the currently selected item is lost and the selection must be changed - /// depending on the property. - /// - private void LostSelection() - { - var items = Items?.Cast(); - var index = -1; - - if (items != null && AlwaysSelected) - { - index = Math.Min(SelectedIndex, items.Count() - 1); - } - - SelectedIndex = index; - } - /// /// Sets a container's 'selected' class or . /// @@ -817,431 +735,98 @@ namespace Avalonia.Controls.Primitives return index; } - private void ResetSelectedItems() - { - UpdateSelectedItems(() => - { - SelectedItems.Clear(); - - foreach (var i in _selection) - { - SelectedItems.Add(ElementAt(Items, i)); - } - }); - } - - /// - /// Called when the CollectionChanged event is raised. - /// - /// The event sender. - /// The event args. - private void SelectedItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + private void UpdateContainerSelection() { - if (_syncingSelectedItems) - { - return; - } - - void Add(IList newItems, IList? addedItems = null) - { - foreach (var item in newItems) - { - var index = MarkItemSelected(item, true); - - if (index != -1 && _selection.Add(index) && addedItems != null) - { - addedItems.Add(item); - } - } - } - - void UpdateSelection() + if (Presenter?.Panel is IPanel panel) { - if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) || - (SelectedIndex == -1 && _selection.HasItems)) + foreach (var container in panel.Children) { - _selectedIndex = _selection.First(); - _selectedItem = ElementAt(Items, _selectedIndex); - RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); + MarkContainerSelected( + container, + Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container))); } } - - IList? added = null; - IList? removed = null; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - { - Add(e.NewItems); - UpdateSelection(); - added = e.NewItems; - } - - break; - - case NotifyCollectionChangedAction.Remove: - if (SelectedItems.Count == 0) - { - SelectedIndex = -1; - } - - foreach (var item in e.OldItems) - { - var index = MarkItemSelected(item, false); - _selection.Remove(index); - } - - removed = e.OldItems; - break; - - case NotifyCollectionChangedAction.Replace: - throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported."); - - case NotifyCollectionChangedAction.Move: - throw new NotSupportedException("Moving items in a SelectedItems collection is not supported."); - - case NotifyCollectionChangedAction.Reset: - { - removed = new List(); - added = new List(); - - foreach (var index in _selection.ToList()) - { - var item = ElementAt(Items, index); - - if (!SelectedItems.Contains(item)) - { - MarkItemSelected(index, false); - removed.Add(item); - _selection.Remove(index); - } - } - - Add(SelectedItems, added); - UpdateSelection(); - } - - break; - } - - if (added?.Count > 0 || removed?.Count > 0) - { - var changed = new SelectionChangedEventArgs( - SelectionChangedEvent, - removed ?? Empty, - added ?? Empty); - RaiseEvent(changed); - } - } - - /// - /// Subscribes to the CollectionChanged event, if any. - /// - private void SubscribeToSelectedItems() - { - var incc = _selectedItems as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged += SelectedItemsCollectionChanged; - } - - SelectedItemsCollectionChanged( - _selectedItems, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - /// - /// Unsubscribes from the CollectionChanged event, if any. - /// - private void UnsubscribeFromSelectedItems() + private ISelectionModel CreateDefaultSelectionModel() { - var incc = _selectedItems as INotifyCollectionChanged; - - if (incc != null) + return new SelectionModel { - incc.CollectionChanged -= SelectedItemsCollectionChanged; - } + SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), + }; } - /// - /// Updates the selection due to a change to or - /// . - /// - /// The new selected index. - /// Whether to clear existing selection. - private void UpdateSelectedItem(int index, bool clear = true) + private void InitializeSelectionModel(ISelectionModel model) { - var oldIndex = _selectedIndex; - var oldItem = _selectedItem; - - if (index == -1 && AlwaysSelected) - { - index = Math.Min(SelectedIndex, ItemCount - 1); - } - - var item = ElementAt(Items, index); - var itemChanged = !Equals(item, oldItem); - var added = -1; - HashSet? removedIndexes = null; - List? removedItems = null; - - _selectedIndex = index; - _selectedItem = item; - - if (oldIndex != index || itemChanged || _selection.HasMultiple) + if (_initializing == 0) { - if (clear) - { - removedIndexes = _selection.Clear(); - } - - if (index != -1) - { - if (_selection.Add(index)) - { - added = index; - } - - if (removedIndexes?.Contains(index) == true) - { - removedIndexes.Remove(index); - added = -1; - } - } - else - { - removedItems ??= new List(); - removedItems.Add(oldItem); - } - - if (removedIndexes != null) - { - foreach (var i in removedIndexes) - { - MarkItemSelected(i, false); - } - } - - MarkItemSelected(index, true); - - RaisePropertyChanged( - SelectedIndexProperty, - oldIndex, - index); + model.Source = Items; } - if (itemChanged) - { - RaisePropertyChanged( - SelectedItemProperty, - oldItem, - item); - } + model.PropertyChanged += OnSelectionModelPropertyChanged; + model.SelectionChanged += OnSelectionModelSelectionChanged; - if (removedIndexes != null && index != -1) + if (model.SingleSelect) { - removedIndexes.Remove(index); + SelectionMode &= ~SelectionMode.Multiple; } - - if (added != -1 || removedItems?.Count > 0 || removedIndexes?.Count > 0) + else { - ResetSelectedItems(); - - if (removedIndexes is object) - { - removedItems ??= new List(); - - foreach (var removed in removedIndexes) - { - var i = ElementAt(Items, removed); - - if (!removedItems.Contains(i)) - { - removedItems.Add(i); - } - } - } - - var e = new SelectionChangedEventArgs( - SelectionChangedEvent, - (IList?)removedItems ?? Array.Empty(), - added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty()); - RaiseEvent(e); + SelectionMode |= SelectionMode.Multiple; } - if (AutoScrollToSelectedItem && _selectedIndex != -1) - { - ScrollIntoView(_selectedItem); - } - } + _oldSelectedIndex = model.SelectedIndex; + _oldSelectedItem = model.SelectedItem; - private void UpdateSelectedItems(Action action) - { - try - { - _syncingSelectedItems = true; - action(); - } - catch (Exception ex) - { - Logger.TryGet(LogEventLevel.Error, LogArea.Property)?.Log( - this, - "Error thrown updating SelectedItems: {Error}", - ex); - } - finally - { - _syncingSelectedItems = false; - } - } + //if (model.AutoSelect) + //{ + // SelectionMode |= SelectionMode.AlwaysSelected; + //} + //else + //{ + // SelectionMode &= ~SelectionMode.AlwaysSelected; + //} - private void UpdateFinished() - { - if (_updateSelectedItem != null) - { - SelectedItem = _updateSelectedItem; - } - else - { - if (ItemCount == 0 && SelectedIndex != -1) - { - SelectedIndex = -1; - } - else - { - if (_updateSelectedIndex != int.MinValue) - { - SelectedIndex = _updateSelectedIndex; - } + //if (Items is INotifyCollectionChanged incc) + //{ + // // At this point we can be sure that SelectionModel has subscribed to collection + // // changes. + // incc.CollectionChanged += AfterItemsCollectionChanged; + //} - if (AlwaysSelected && SelectedIndex == -1) - { - SelectedIndex = 0; - } - } - } - } + //UpdateContainerSelection(); - private void InternalBeginInit() - { - if (_updateCount == 0) - { - _updateSelectedIndex = int.MinValue; - } + //var selectedIndex = SelectedIndex; + //var selectedItem = SelectedItem; - ++_updateCount; - } + //if (_selectedIndex != selectedIndex) + //{ + // RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, selectedIndex); + // _selectedIndex = selectedIndex; + //} - private void InternalEndInit() - { - Debug.Assert(_updateCount > 0); + //if (_selectedItem != selectedItem) + //{ + // RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem); + // _selectedItem = selectedItem; + //} - if (--_updateCount == 0) - { - UpdateFinished(); - } + //if (selectedIndex != -1) + //{ + // RaiseEvent(new SelectionChangedEventArgs( + // SelectionChangedEvent, + // Array.Empty(), + // Selection.SelectedItems.ToList())); + //} } - private class Selection : IEnumerable + private void DeinitializeSelectionModel(ISelectionModel? model) { - private readonly List _list = new List(); - private HashSet _set = new HashSet(); - - public bool HasItems => _set.Count > 0; - public bool HasMultiple => _set.Count > 1; - - public bool Add(int index) - { - if (index == -1) - { - throw new ArgumentException("Invalid index", "index"); - } - - if (_set.Add(index)) - { - _list.Add(index); - return true; - } - - return false; - } - - public bool Remove(int index) - { - if (_set.Remove(index)) - { - _list.RemoveAll(x => x == index); - return true; - } - - return false; - } - - public HashSet Clear() + if (model is object) { - var result = _set; - _list.Clear(); - _set = new HashSet(); - return result; + model.PropertyChanged -= OnSelectionModelPropertyChanged; + model.SelectionChanged -= OnSelectionModelSelectionChanged; } - - public void ItemsInserted(int index, int count) - { - _set = new HashSet(); - - for (var i = 0; i < _list.Count; ++i) - { - var ix = _list[i]; - - if (ix >= index) - { - var newIndex = ix + count; - _list[i] = newIndex; - _set.Add(newIndex); - } - else - { - _set.Add(ix); - } - } - } - - public void ItemsRemoved(int index, int count) - { - var last = (index + count) - 1; - - _set = new HashSet(); - - for (var i = 0; i < _list.Count; ++i) - { - var ix = _list[i]; - - if (ix >= index && ix <= last) - { - _list.RemoveAt(i--); - } - else if (ix > last) - { - var newIndex = ix - count; - _list[i] = newIndex; - _set.Add(newIndex); - } - else - { - _set.Add(ix); - } - } - } - - public bool Contains(int index) => _set.Contains(index); - - public int First() => HasItems ? _list[0] : -1; - - public IEnumerator GetEnumerator() => _set.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } } diff --git a/src/Avalonia.Controls/Selection/ISelectionModel.cs b/src/Avalonia.Controls/Selection/ISelectionModel.cs index 8635b7f6e2..3b8fd0c8b7 100644 --- a/src/Avalonia.Controls/Selection/ISelectionModel.cs +++ b/src/Avalonia.Controls/Selection/ISelectionModel.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Selection bool SingleSelect { get; set; } int SelectedIndex { get; set; } IReadOnlyList SelectedIndexes { get; } - object? SelectedItem { get; } + object? SelectedItem { get; set; } IReadOnlyList SelectedItems { get; } int AnchorIndex { get; set; } int Count { get; } @@ -30,19 +30,37 @@ namespace Avalonia.Controls.Selection void Deselect(int index); void SelectRange(int start, int end); void DeselectRange(int start, int end); + void SelectAll(); void Clear(); } public static class SelectionModelExtensions { - public static void SelectAll(this ISelectionModel model) + public static IDisposable BatchUpdate(this ISelectionModel model) { - model.SelectRange(0, int.MaxValue); + return new BatchUpdateOperation(model); } - public static void SelectRangeFromAnchor(this ISelectionModel model, int to) + public struct BatchUpdateOperation : IDisposable { - model.SelectRange(model.AnchorIndex, to); + private readonly ISelectionModel _owner; + private bool _isDisposed; + + public BatchUpdateOperation(ISelectionModel owner) + { + _owner = owner; + _isDisposed = false; + owner.BeginBatchUpdate(); + } + + public void Dispose() + { + if (!_isDisposed) + { + _owner?.EndBatchUpdate(); + _isDisposed = true; + } + } } } } diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index d6af813107..68fe34536a 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -20,6 +20,8 @@ namespace Avalonia.Controls.Selection private SelectedItems? _selectedItems; private SelectedItems.Untyped? _selectedItemsUntyped; private EventHandler? _untypedSelectionChanged; + [AllowNull] private T _initSelectedItem = default; + private bool _hasInitSelectedItem; public SelectionModel() { @@ -51,7 +53,18 @@ namespace Avalonia.Controls.Selection using var update = BatchUpdate(); update.Operation.IsSourceUpdate = true; - TrimInvalidSelections(update.Operation); + + if (_hasInitSelectedItem) + { + SelectedItem = _initSelectedItem; + _initSelectedItem = default; + _hasInitSelectedItem = false; + } + else + { + TrimInvalidSelections(update.Operation); + } + RaisePropertyChanged(nameof(Source)); } } @@ -90,10 +103,37 @@ namespace Avalonia.Controls.Selection public IReadOnlyList SelectedIndexes => _selectedIndexes ??= new SelectedIndexes(this); - [MaybeNull] - public T SelectedItem => GetItemAt(_selectedIndex); + [MaybeNull, AllowNull] + public T SelectedItem + { + get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem; + set + { + if (ItemsView is object) + { + SelectedIndex = ItemsView.IndexOf(value!); + } + else + { + Clear(); + _initSelectedItem = value; + _hasInitSelectedItem = true; + } + } + } - public IReadOnlyList SelectedItems => _selectedItems ??= new SelectedItems(this); + public IReadOnlyList SelectedItems + { + get + { + if (ItemsView is null && _hasInitSelectedItem) + { + return new[] { _initSelectedItem }; + } + + return _selectedItems ??= new SelectedItems(this); + } + } public int AnchorIndex { @@ -127,7 +167,22 @@ namespace Avalonia.Controls.Selection set => Source = (IEnumerable?)value; } - object? ISelectionModel.SelectedItem => SelectedItem; + object? ISelectionModel.SelectedItem + { + get => SelectedItem; + set + { + if (value is T t) + { + SelectedItem = t; + } + else + { + SelectedIndex = -1; + } + } + + } IReadOnlyList ISelectionModel.SelectedItems { @@ -226,8 +281,12 @@ namespace Avalonia.Controls.Selection { o.SelectedIndex = -1; } + + _initSelectedItem = default; + _hasInitSelectedItem = false; } + public void SelectAll() => SelectRange(0, int.MaxValue); public void Clear() => DeselectRange(0, int.MaxValue); protected void RaisePropertyChanged(string propertyName) @@ -429,6 +488,9 @@ namespace Avalonia.Controls.Selection { o.SelectedIndex = o.AnchorIndex = start; } + + _initSelectedItem = default; + _hasInitSelectedItem = false; } [return: MaybeNull] diff --git a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs new file mode 100644 index 0000000000..25428aaee6 --- /dev/null +++ b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using Avalonia.Collections; +using Avalonia.Controls.Selection; + +#nullable enable + +namespace Avalonia.Controls.Utils +{ + /// + /// Synchronizes an with a list of SelectedItems. + /// + internal class SelectedItemsSync + { + private IList? _selectedItems; + private bool _updatingItems; + private bool _updatingModel; + private bool _initializeOnSourceAssignment; + + public SelectedItemsSync(ISelectionModel model) + { + model = model ?? throw new ArgumentNullException(nameof(model)); + Model = model; + } + + public ISelectionModel Model { get; private set; } + + public IList GetOrCreateSelectedItems() + { + if (_selectedItems == null) + { + var items = new AvaloniaList(Model.SelectedItems); + items.CollectionChanged += ItemsCollectionChanged; + Model.SelectionChanged += SelectionModelSelectionChanged; + _selectedItems = items; + } + + return _selectedItems; + } + + public void SetSelectedItems(IList? items) + { + items ??= new AvaloniaList(); + + if (items.IsFixedSize) + { + throw new NotSupportedException( + "Cannot assign fixed size selection to SelectedItems."); + } + + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= ItemsCollectionChanged; + } + + if (_selectedItems == null) + { + Model.SelectionChanged += SelectionModelSelectionChanged; + } + + try + { + _updatingModel = true; + _selectedItems = items; + + if (Model.Source is object) + { + using (Model.BatchUpdate()) + { + Model.Clear(); + Add(items); + } + } + else if (!_initializeOnSourceAssignment) + { + Model.PropertyChanged += SelectionModelPropertyChanged; + _initializeOnSourceAssignment = true; + } + + if (_selectedItems is INotifyCollectionChanged incc2) + { + incc2.CollectionChanged += ItemsCollectionChanged; + } + } + finally + { + _updatingModel = false; + } + } + + public void SetModel(ISelectionModel model) + { + model = model ?? throw new ArgumentNullException(nameof(model)); + + if (_selectedItems != null) + { + Model.PropertyChanged -= SelectionModelPropertyChanged; + Model.SelectionChanged -= SelectionModelSelectionChanged; + Model = model; + Model.SelectionChanged += SelectionModelSelectionChanged; + _initializeOnSourceAssignment = false; + + try + { + _updatingItems = true; + _selectedItems.Clear(); + + foreach (var i in model.SelectedItems) + { + _selectedItems.Add(i); + } + } + finally + { + _updatingItems = false; + } + } + } + + private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (_updatingItems) + { + return; + } + + if (_selectedItems == null) + { + throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); + } + + void Remove() + { + foreach (var i in e.OldItems) + { + var index = IndexOf(Model.Source, i); + + if (index != -1) + { + Model.Deselect(index); + } + } + } + + try + { + using var operation = Model.BatchUpdate(); + + _updatingModel = true; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + Remove(); + break; + case NotifyCollectionChangedAction.Replace: + Remove(); + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + Model.Clear(); + Add(_selectedItems); + break; + } + } + finally + { + _updatingModel = false; + } + } + + private void Add(IList newItems) + { + foreach (var i in newItems) + { + var index = IndexOf(Model.Source, i); + + if (index != -1) + { + Model.Select(index); + } + } + } + + private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_initializeOnSourceAssignment && + _selectedItems != null && + e.PropertyName == nameof(ISelectionModel.Source)) + { + try + { + _updatingModel = true; + Add(_selectedItems); + _initializeOnSourceAssignment = false; + } + finally + { + _updatingModel = false; + } + } + } + + private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + if (_updatingModel) + { + return; + } + + if (_selectedItems == null) + { + throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items."); + } + + try + { + var deselected = e.DeselectedItems.ToList(); + var selected = e.SelectedItems.ToList(); + + _updatingItems = true; + + foreach (var i in deselected) + { + _selectedItems.Remove(i); + } + + foreach (var i in selected) + { + _selectedItems.Add(i); + } + } + finally + { + _updatingItems = false; + } + } + + private static int IndexOf(object? source, object? item) + { + if (source is IList l) + { + return l.IndexOf(item); + } + else if (source is ItemsSourceView v) + { + return v.IndexOf(item); + } + + return -1; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 4ad384b054..3a0a915558 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -19,7 +19,7 @@ using Xunit; namespace Avalonia.Controls.UnitTests.Primitives { - public class SelectingItemsControlTests + public partial class SelectingItemsControlTests { private MouseTestHelper _helper = new MouseTestHelper(); @@ -56,7 +56,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; - target.ApplyTemplate(); + Prepare(target); Assert.False(items[0].IsSelected); Assert.False(items[1].IsSelected); @@ -77,8 +77,8 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); + target.SelectedItem = items[1]; Assert.False(items[0].IsSelected); @@ -101,8 +101,7 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.SelectedItem = items[1]; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); Assert.False(items[0].IsSelected); Assert.True(items[1].IsSelected); @@ -159,6 +158,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Template = Template(); target.EndInit(); + Prepare(target); + Assert.Equal(0, target.SelectedIndex); } @@ -181,6 +182,8 @@ namespace Avalonia.Controls.UnitTests.Primitives listBox.EndInit(); + Prepare(listBox); + Assert.Equal("B", listBox.SelectedItem); } @@ -223,7 +226,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Setting_SelectedItem_Before_Initialize_Should_Retain() + public void Setting_SelectedItem_Before_Initialize_Should_Retain_Selection() { var listBox = new ListBox { @@ -290,7 +293,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain() + public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain_Selection() { var listBox = new ListBox { @@ -324,8 +327,7 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.SelectedIndex = 1; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); Assert.False(items[0].IsSelected); Assert.True(items[1].IsSelected); @@ -480,8 +482,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); items.Add(new Item { IsSelected = true }); Assert.Equal(2, target.SelectedIndex); @@ -530,8 +531,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); @@ -568,8 +568,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Template = Template(); target.EndInit(); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedIndex = 0; Assert.Equal(items[0], target.SelectedItem); @@ -635,8 +634,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItem = items[1]; Assert.False(items[0].IsSelected); @@ -666,8 +664,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); target.SelectedItem = items[1]; Assert.False(items[0].IsSelected); @@ -757,8 +754,7 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectedIndex = 1, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); var called = false; @@ -897,8 +893,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = new[] { "Foo", "Bar", "Baz " }, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); _helper.Down((Interactive)target.Presenter.Panel.Children[1]); var panel = target.Presenter.Panel; @@ -919,8 +914,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = items, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); _helper.Down(target.Presenter.Panel.Children[1]); @@ -1014,8 +1008,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); Assert.Equal(3, target.SelectedIndex); @@ -1030,8 +1023,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes); @@ -1054,8 +1046,7 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectedIndex = 1, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); items.Insert(0, "Qux"); @@ -1080,8 +1071,7 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectedIndex = 1, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); items.RemoveAt(0); @@ -1109,8 +1099,7 @@ namespace Avalonia.Controls.UnitTests.Primitives [!ListBox.SelectedIndexProperty] = other[!ListBox.SelectedIndexProperty], }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); other.ApplyTemplate(); other.Presenter.ApplyTemplate(); @@ -1140,8 +1129,7 @@ namespace Avalonia.Controls.UnitTests.Primitives [!ListBox.SelectedItemProperty] = other[!ListBox.SelectedItemProperty], }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); other.ApplyTemplate(); other.Presenter.ApplyTemplate(); @@ -1168,8 +1156,7 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectedIndex = 1, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); items[1] = "Qux"; @@ -1193,8 +1180,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = items, }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); var raised = false; target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); @@ -1323,16 +1309,36 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Items = items; - target.ApplyTemplate(); - - target.Presenter.ApplyTemplate(); + Prepare(target); Assert.Equal(second, target.SelectedItem); Assert.Equal(1, target.SelectedIndex); } - private FuncControlTemplate Template() + private static void Prepare(SelectingItemsControl target) + { + var root = new TestRoot + { + Child = target, + Width = 100, + Height = 100, + Styles = + { + new Style(x => x.Is()) + { + Setters = + { + new Setter(ListBox.TemplateProperty, Template()), + }, + }, + }, + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + } + + private static FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => new ItemsPresenter diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index d4155206f2..656d7c05fa 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1304,8 +1304,8 @@ namespace Avalonia.Controls.UnitTests.Primitives set { base.SelectionMode = value; } } - public new void SelectAll() => base.SelectAll(); - public new void UnselectAll() => base.UnselectAll(); + public void SelectAll() => Selection.SelectAll(); + public void UnselectAll() => Selection.Clear(); public void SelectRange(int index) => UpdateSelection(index, true, true); public void Toggle(int index) => UpdateSelection(index, true, false, true); } diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs index f07d2cddea..3644856917 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs @@ -120,6 +120,38 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(0, raised); } + + [Fact] + public void Initializing_Source_Respects_Range_SourceItem_Order() + { + var target = CreateTarget(false); + + target.SelectRange(2, 2); + target.SelectedItem = "bar"; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { 1 }, target.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + } + + [Fact] + public void Initializing_Source_Respects_SourceItem_Range_Order() + { + var target = CreateTarget(false); + + target.SelectedItem = "baz"; + target.SelectRange(1, 1); + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { 1 }, target.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + } } public class SelectedIndex diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 9f301131b7..a667b17b1e 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -4,6 +4,7 @@ using Avalonia.Collections; using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; using Xunit; +using CollectionChangedEventManager = Avalonia.Controls.Utils.CollectionChangedEventManager; #nullable enable @@ -14,7 +15,7 @@ namespace Avalonia.Controls.UnitTests.Selection public class Source { [Fact] - public void Can_Select_Item_Before_Source_Assigned() + public void Can_Select_Index_Before_Source_Assigned() { var target = CreateTarget(false); var raised = 0; @@ -38,7 +39,23 @@ namespace Avalonia.Controls.UnitTests.Selection } [Fact] - public void Initializing_Source_Retains_Valid_Selection() + public void Can_Select_Item_Before_Source_Assigned() + { + var target = CreateTarget(false); + var raised = 0; + + target.SelectionChanged += (s, e) => ++raised; + target.SelectedItem = "bar"; + + Assert.Equal(-1, target.SelectedIndex); + Assert.Empty(target.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new string?[] { "bar" }, target.SelectedItems); + Assert.Equal(0, raised); + } + + [Fact] + public void Initializing_Source_Retains_Valid_Index_Selection() { var target = CreateTarget(false); var raised = 0; @@ -57,7 +74,7 @@ namespace Avalonia.Controls.UnitTests.Selection } [Fact] - public void Initializing_Source_Removes_Invalid_Selection() + public void Initializing_Source_Removes_Invalid_Index_Selection() { var target = CreateTarget(false); var raised = 0; @@ -82,6 +99,81 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, raised); } + [Fact] + public void Initializing_Source_Retains_Valid_Item_Selection() + { + var target = CreateTarget(false); + var raised = 0; + + target.SelectedItem = "bar"; + + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.DeselectedIndexes); + Assert.Empty(e.DeselectedItems); + Assert.Equal(new[] { 1 }, e.SelectedIndexes); + Assert.Equal(new string[] { "bar" }, e.SelectedItems); + ++raised; + }; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { 1 }, target.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(1, raised); + } + + [Fact] + public void Initializing_Source_Removes_Invalid_Item_Selection() + { + var target = CreateTarget(false); + var raised = 0; + + target.SelectedItem = "qux"; + target.SelectionChanged += (s, e) => ++raised; + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(-1, target.SelectedIndex); + Assert.Empty(target.SelectedIndexes); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + Assert.Equal(0, raised); + } + + [Fact] + public void Initializing_Source_Respects_SourceIndex_SourceItem_Order() + { + var target = CreateTarget(false); + + target.SelectedIndex = 0; + target.SelectedItem = "bar"; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { 1 }, target.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + } + + [Fact] + public void Initializing_Source_Respects_SourceItem_SourceIndex_Order() + { + var target = CreateTarget(false); + + target.SelectedItem = "foo"; + target.SelectedIndex = 1; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { 1 }, target.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + } + [Fact] public void Changing_Source_First_Clears_Old_Selection() { @@ -240,7 +332,7 @@ namespace Avalonia.Controls.UnitTests.Selection public MockBinding(SelectionModel target, AvaloniaList data) { _target = target; - Avalonia.Controls.Utils.CollectionChangedEventManager.Instance.AddListener(data, this); + CollectionChangedEventManager.Instance.AddListener(data, this); } public void Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -260,6 +352,26 @@ namespace Avalonia.Controls.UnitTests.Selection public class SelectedItem { + [Fact] + public void Setting_SelectedItem_To_Valid_Item_Updates_Selection() + { + var target = CreateTarget(); + var raised = 0; + + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.DeselectedIndexes); + Assert.Empty(e.DeselectedItems); + Assert.Equal(new[] { 1 }, e.SelectedIndexes); + Assert.Equal(new[] { "bar" }, e.SelectedItems); + ++raised; + }; + + target.SelectedItem = "bar"; + + Assert.Equal(1, raised); + } + [Fact] public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes() {