Browse Source

SelectedItems and SelectedIndexes implement INCC. (#15498)

Make `SelectionModel.SelectedItems` and `SelectionModel.SelectedIndexes` implement `INotifyCollectionChanged` so that they can be bound to.

As well as implementing `INotifyCollectionChanged` on the collections, we also had to implement `IList` (see #8764) so refactored this out into a base class.

For the sake of simplicity, these collections only raise `Reset` for any change: this is may need to be changed later but I'd rather follow the KISS principle for the moment until something more complex is proven necessary.

Fixes #15497
Steven Kirk 1 year ago
parent
commit
9287ab5913

+ 58 - 0
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<T> : IReadOnlyList<T?>, 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<T?> 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.");
+}

+ 4 - 6
src/Avalonia.Controls/Selection/SelectedIndexes.cs

@@ -5,7 +5,7 @@ using System.Linq;
 
 namespace Avalonia.Controls.Selection
 {
-    internal class SelectedIndexes<T> : IReadOnlyList<int>
+    internal class SelectedIndexes<T> : ReadOnlySelectionListBase<int>
     {
         private readonly SelectionModel<T>? _owner;
         private readonly IReadOnlyList<IndexRange>? _ranges;
@@ -13,7 +13,7 @@ namespace Avalonia.Controls.Selection
         public SelectedIndexes(SelectionModel<T> owner) => _owner = owner;
         public SelectedIndexes(IReadOnlyList<IndexRange> 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<IndexRange> Ranges => _ranges ?? _owner!.Ranges!;
 
-        public IEnumerator<int> GetEnumerator()
+        public override IEnumerator<int> GetEnumerator()
         {
             IEnumerator<int> SingleSelect()
             {
@@ -74,7 +74,5 @@ namespace Avalonia.Controls.Selection
         {
             return ranges is object ? new SelectedIndexes<T>(ranges) : null;
         }
-
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
     }
 }

+ 10 - 12
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<T> : IReadOnlyList<T?>
+    internal class SelectedItems<T> : ReadOnlySelectionListBase<T>
     {
         private readonly SelectionModel<T>? _owner;
         private readonly ItemsSourceView<T>? _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<T>? Items => _items ?? _owner?.ItemsView;
         private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
 
-        public IEnumerator<T?> GetEnumerator()
+        public override IEnumerator<T?> GetEnumerator()
         {
             if (_owner?.SingleSelect == true)
             {
@@ -84,8 +85,6 @@ namespace Avalonia.Controls.Selection
             }
         }
 
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
         public static SelectedItems<T>? Create(
             IReadOnlyList<IndexRange>? ranges,
             ItemsSourceView<T>? items)
@@ -93,14 +92,13 @@ namespace Avalonia.Controls.Selection
             return ranges is object ? new SelectedItems<T>(ranges, items) : null;
         }
 
-        public class Untyped : IReadOnlyList<object?>
+        public class Untyped : ReadOnlySelectionListBase<object?>
         {
             private readonly IReadOnlyList<T?> _source;
             public Untyped(IReadOnlyList<T?> source) => _source = source;
-            public object? this[int index] => _source[index];
-            public int Count => _source.Count;
-            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-            public IEnumerator<object?> GetEnumerator()
+            public override object? this[int index] => _source[index];
+            public override int Count => _source.Count;
+            public override IEnumerator<object?> GetEnumerator()
             {
                 foreach (var i in _source)
                 {

+ 2 - 0
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();
                     }
                 } 
                 

+ 40 - 0
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<INotifyCollectionChanged>(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<INotifyCollectionChanged>(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