Repository: AvaloniaUI/Avalonia

Issue:
Unable to bind to `SelectionModel.SelectedItems` or `SelectedIndexes`
### Describe the bug

Reported by a customer:

Binding `ItemsControl.ItemsSource` to `SelectionModel.SelectedItems` or `SelectionModel.SelectedIndexes` does not work.

`SelectionModel` raises a `PropertyChanged` event on `SelectedItems` and `SelectedIndexes`, but because the instance of the collection hasn't changed, property system doesn't consider the value to have changed, so consequently the bound `ItemsControl` doesn't register the change. This behaviour comes from WinUI which was the original source of our `SelectionModel` (though they have diverged significantly since the initial port):

https://github.com/microsoft/microsoft-ui-xaml/blob/winui3/release/1.4-stable/controls/dev/Repeater/SelectionModel.cpp#L591-L592

I assume that the WinUI `SelectionModel` was written without the expectation that someone would bind to the selected collections on a `SelectionModel`.


### To Reproduce

```xml
<Grid ColumnDefinitions="*,*">
    <ListBox Name="lb" ItemsSource="{Binding}" SelectionMode="Multiple"/>
    <ListBox Grid.Column="1" ItemsSource="{Binding #lb.Selection.SelectedIndexes}"/>
</Grid>
```

### Expected behavior

`SelectionModel.SelectedItems` or `SelectionModel.SelectedIndexes` and should follow the usual semantics for property and collection changes, not the strange semantics we have inherited from WinUI.

### Avalonia version

all

### OS

_No response_

### Additional context

_No response_


Hints:

Patch:
diff --git a/src/Avalonia.Controls/Selection/ReadOnlySelectionListBase.cs b/src/Avalonia.Controls/Selection/ReadOnlySelectionListBase.cs
new file mode 100644
index 00000000000..f603fd81686
--- /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<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.");
+}
diff --git a/src/Avalonia.Controls/Selection/SelectedIndexes.cs b/src/Avalonia.Controls/Selection/SelectedIndexes.cs
index a65f45e64f4..b742d91e218 100644
--- a/src/Avalonia.Controls/Selection/SelectedIndexes.cs
+++ b/src/Avalonia.Controls/Selection/SelectedIndexes.cs
@@ -5,7 +5,7 @@
 
 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 @@ internal class SelectedIndexes<T> : IReadOnlyList<int>
         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 @@ public int this[int index]
             }
         }
 
-        public int Count
+        public override int Count
         {
             get
             {
@@ -50,7 +50,7 @@ public int Count
 
         private IReadOnlyList<IndexRange> Ranges => _ranges ?? _owner!.Ranges!;
 
-        public IEnumerator<int> GetEnumerator()
+        public override IEnumerator<int> GetEnumerator()
         {
             IEnumerator<int> SingleSelect()
             {
@@ -74,7 +74,5 @@ IEnumerator<int> SingleSelect()
         {
             return ranges is object ? new SelectedIndexes<T>(ranges) : null;
         }
-
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
     }
 }
diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs
index 74007805cd5..a892876b295 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<T> : IReadOnlyList<T?>
+    internal class SelectedItems<T> : ReadOnlySelectionListBase<T>
     {
         private readonly SelectionModel<T>? _owner;
         private readonly ItemsSourceView<T>? _items;
@@ -19,7 +20,7 @@ public SelectedItems(IReadOnlyList<IndexRange> ranges, ItemsSourceView<T>? items
             _items = items;
         }
 
-        public T? this[int index]
+        public override T? this[int index]
         {
             get
             {
@@ -43,7 +44,7 @@ public T? this[int index]
             }
         }
 
-        public int Count
+        public override int Count
         {
             get
             {
@@ -61,7 +62,7 @@ public int Count
         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 @@ public int Count
             }
         }
 
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
         public static SelectedItems<T>? Create(
             IReadOnlyList<IndexRange>? ranges,
             ItemsSourceView<T>? items)
@@ -93,14 +92,13 @@ public int Count
             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)
                 {
diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs
index 818308793b9..d191f79801b 100644
--- a/src/Avalonia.Controls/Selection/SelectionModel.cs
+++ b/src/Avalonia.Controls/Selection/SelectionModel.cs
@@ -710,11 +710,13 @@ private void CommitOperation(Operation operation, bool raisePropertyChanged = tr
                     if (indexesChanged)
                     {
                         RaisePropertyChanged(nameof(SelectedIndexes));
+                        _selectedIndexes?.RaiseCollectionReset();
                     }
 
                     if (indexesChanged || operation.IsSourceUpdate)
                     {
                         RaisePropertyChanged(nameof(SelectedItems));
+                        _selectedItems?.RaiseCollectionReset();
                     }
                 } 
                 


Test patch:
diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
index 64236268f8e..f4dbdf54180 100644
--- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
+++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
@@ -516,6 +516,26 @@ public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
 
                 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 @@ public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
 
                 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