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()
{