diff --git a/src/Avalonia.Controls/Selection/ReadOnlySelectionListBase.cs b/src/Avalonia.Controls/Selection/ReadOnlySelectionListBase.cs new file mode 100644 index 0000000000..f603fd8168 --- /dev/null +++ b/src/Avalonia.Controls/Selection/ReadOnlySelectionListBase.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Collections; + +namespace Avalonia.Controls.Selection; + +internal abstract class ReadOnlySelectionListBase : IReadOnlyList, IList, INotifyCollectionChanged +{ + public abstract T? this[int index] { get; } + public abstract int Count { get; } + + object? IList.this[int index] + { + get => this[index]; + set => ThrowReadOnlyException(); + } + + bool IList.IsFixedSize => false; + bool IList.IsReadOnly => true; + bool ICollection.IsSynchronized => false; + object ICollection.SyncRoot => this; + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public abstract IEnumerator GetEnumerator(); + public void RaiseCollectionReset() => CollectionChanged?.Invoke(this, EventArgsCache.ResetCollectionChanged); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + int IList.Add(object? value) { ThrowReadOnlyException(); return 0; } + void IList.Clear() => ThrowReadOnlyException(); + void IList.Insert(int index, object? value) => ThrowReadOnlyException(); + void IList.Remove(object? value) => ThrowReadOnlyException(); + void IList.RemoveAt(int index) => ThrowReadOnlyException(); + bool IList.Contains(object? value) => Count != 0 && ((IList)this).IndexOf(value) != -1; + + void ICollection.CopyTo(Array array, int index) + { + foreach (var item in this) + array.SetValue(item, index++); + } + + int IList.IndexOf(object? value) + { + for (int i = 0; i < Count; i++) + { + if (Equals(this[i], value)) + return i; + } + + return -1; + } + + [DoesNotReturn] + private static void ThrowReadOnlyException() => throw new NotSupportedException("Collection is read-only."); +} diff --git a/src/Avalonia.Controls/Selection/SelectedIndexes.cs b/src/Avalonia.Controls/Selection/SelectedIndexes.cs index a65f45e64f..b742d91e21 100644 --- a/src/Avalonia.Controls/Selection/SelectedIndexes.cs +++ b/src/Avalonia.Controls/Selection/SelectedIndexes.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Avalonia.Controls.Selection { - internal class SelectedIndexes : IReadOnlyList + internal class SelectedIndexes : ReadOnlySelectionListBase { private readonly SelectionModel? _owner; private readonly IReadOnlyList? _ranges; @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Selection public SelectedIndexes(SelectionModel owner) => _owner = owner; public SelectedIndexes(IReadOnlyList ranges) => _ranges = ranges; - public int this[int index] + public override int this[int index] { get { @@ -33,7 +33,7 @@ namespace Avalonia.Controls.Selection } } - public int Count + public override int Count { get { @@ -50,7 +50,7 @@ namespace Avalonia.Controls.Selection private IReadOnlyList Ranges => _ranges ?? _owner!.Ranges!; - public IEnumerator GetEnumerator() + public override IEnumerator GetEnumerator() { IEnumerator SingleSelect() { @@ -74,7 +74,5 @@ namespace Avalonia.Controls.Selection { return ranges is object ? new SelectedIndexes(ranges) : null; } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs index 74007805cd..a892876b29 100644 --- a/src/Avalonia.Controls/Selection/SelectedItems.cs +++ b/src/Avalonia.Controls/Selection/SelectedItems.cs @@ -1,11 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Collections.Specialized; +using Avalonia.Collections; namespace Avalonia.Controls.Selection { - internal class SelectedItems : IReadOnlyList + internal class SelectedItems : ReadOnlySelectionListBase { private readonly SelectionModel? _owner; private readonly ItemsSourceView? _items; @@ -19,7 +20,7 @@ namespace Avalonia.Controls.Selection _items = items; } - public T? this[int index] + public override T? this[int index] { get { @@ -43,7 +44,7 @@ namespace Avalonia.Controls.Selection } } - public int Count + public override int Count { get { @@ -61,7 +62,7 @@ namespace Avalonia.Controls.Selection private ItemsSourceView? Items => _items ?? _owner?.ItemsView; private IReadOnlyList? Ranges => _ranges ?? _owner!.Ranges; - public IEnumerator GetEnumerator() + public override IEnumerator GetEnumerator() { if (_owner?.SingleSelect == true) { @@ -84,8 +85,6 @@ namespace Avalonia.Controls.Selection } } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public static SelectedItems? Create( IReadOnlyList? ranges, ItemsSourceView? items) @@ -93,14 +92,13 @@ namespace Avalonia.Controls.Selection return ranges is object ? new SelectedItems(ranges, items) : null; } - public class Untyped : IReadOnlyList + public class Untyped : ReadOnlySelectionListBase { private readonly IReadOnlyList _source; public Untyped(IReadOnlyList source) => _source = source; - public object? this[int index] => _source[index]; - public int Count => _source.Count; - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public IEnumerator GetEnumerator() + public override object? this[int index] => _source[index]; + public override int Count => _source.Count; + public override IEnumerator GetEnumerator() { foreach (var i in _source) { diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 818308793b..d191f79801 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -710,11 +710,13 @@ namespace Avalonia.Controls.Selection if (indexesChanged) { RaisePropertyChanged(nameof(SelectedIndexes)); + _selectedIndexes?.RaiseCollectionReset(); } if (indexesChanged || operation.IsSourceUpdate) { RaisePropertyChanged(nameof(SelectedItems)); + _selectedItems?.RaiseCollectionReset(); } } diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 64236268f8..f4dbdf5418 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -516,6 +516,26 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, raised); } + + [Fact] + public void CollectionChanged_Is_Raised_When_SelectedIndex_Changes() + { + var target = CreateTarget(); + var raised = 0; + var incc = Assert.IsAssignableFrom(target.SelectedIndexes); + + incc.CollectionChanged += (s, e) => + { + // For the moment, for simplicity, we raise a Reset event when the SelectedIndexes + // collection changes - whatever the change. This can be improved later if necessary. + Assert.Equal(NotifyCollectionChangedAction.Reset, e.Action); + ++raised; + }; + + target.SelectedIndex = 1; + + Assert.Equal(1, raised); + } } public class SelectedItems @@ -538,6 +558,26 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, raised); } + + [Fact] + public void CollectionChanged_Is_Raised_When_SelectedIndex_Changes() + { + var target = CreateTarget(); + var raised = 0; + var incc = Assert.IsAssignableFrom(target.SelectedIndexes); + + incc.CollectionChanged += (s, e) => + { + // For the moment, for simplicity, we raise a Reset event when the SelectedItems + // collection changes - whatever the change. This can be improved later if necessary. + Assert.Equal(NotifyCollectionChangedAction.Reset, e.Action); + ++raised; + }; + + target.SelectedIndex = 1; + + Assert.Equal(1, raised); + } } public class Select