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
release/11.1.0-beta2
Steven Kirk 2 years ago
committed by Max Katz
parent
commit
462daa38b8
  1. 58
      src/Avalonia.Controls/Selection/ReadOnlySelectionListBase.cs
  2. 10
      src/Avalonia.Controls/Selection/SelectedIndexes.cs
  3. 22
      src/Avalonia.Controls/Selection/SelectedItems.cs
  4. 2
      src/Avalonia.Controls/Selection/SelectionModel.cs
  5. 40
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

58
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.");
}

10
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();
}
}

22
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
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
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

Loading…
Cancel
Save