Browse Source

Fix working with collections of value types.

`IEnumerable<T>` is not covariant for value types, so we need to use the non-generic `IEnumerable` everywhere under the hood to ensure we can work with both value and reference types.
pull/4533/head
Steven Kirk 6 years ago
parent
commit
e4f03fdf79
  1. 101
      src/Avalonia.Controls/ItemsSourceView.cs
  2. 86
      src/Avalonia.Controls/Selection/SelectionModel.cs
  3. 4
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  4. 8
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

101
src/Avalonia.Controls/ItemsSourceView.cs

@ -7,6 +7,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
@ -15,29 +16,30 @@ using Avalonia.Controls.Utils;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Represents a standardized view of the supported interactions between a given /// Represents a standardized view of the supported interactions between a given ItemsSource
/// <see cref="ItemsControl"/> or <see cref="ItemsRepeater"/> and its items. /// object and an <see cref="ItemsRepeater"/> control.
/// </summary> /// </summary>
public class ItemsSourceView<T> : INotifyCollectionChanged, IDisposable, IReadOnlyList<T> /// <remarks>
/// Components written to work with ItemsRepeater should consume the
/// <see cref="ItemsRepeater.Items"/> via ItemsSourceView since this provides a normalized
/// view of the Items. That way, each component does not need to know if the source is an
/// IEnumerable, an IList, or something else.
/// </remarks>
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
{ {
/// <summary> /// <summary>
/// Gets an empty <see cref="ItemsSourceView{T}"/> /// Gets an empty <see cref="ItemsSourceView"/>
/// </summary> /// </summary>
public static ItemsSourceView<T> Empty { get; } = new ItemsSourceView<T>(Array.Empty<T>()); public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
private readonly IList _inner; private protected readonly IList _inner;
private INotifyCollectionChanged? _notifyCollectionChanged; private INotifyCollectionChanged? _notifyCollectionChanged;
/// <summary> /// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source. /// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary> /// </summary>
/// <param name="source">The data source.</param> /// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable<T> source) public ItemsSourceView(IEnumerable source)
: this((IEnumerable)source)
{
}
private protected ItemsSourceView(IEnumerable source)
{ {
source = source ?? throw new ArgumentNullException(nameof(source)); source = source ?? throw new ArgumentNullException(nameof(source));
@ -45,13 +47,13 @@ namespace Avalonia.Controls
{ {
_inner = list; _inner = list;
} }
else if (source is IEnumerable<object?> enumerable) else if (source is IEnumerable<object> objectEnumerable)
{ {
_inner = new List<object?>(enumerable); _inner = new List<object>(objectEnumerable);
} }
else else
{ {
_inner = new List<object?>(source.Cast<object?>()); _inner = new List<object>(source.Cast<object>());
} }
ListenToCollectionChanges(); ListenToCollectionChanges();
@ -75,7 +77,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
/// <returns>The item.</returns> /// <returns>The item.</returns>
public T this[int index] => GetAt(index); public object? this[int index] => GetAt(index);
/// <summary> /// <summary>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed. /// Occurs when the collection has changed to indicate the reason for the change and which items changed.
@ -96,13 +98,13 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
/// <returns>The item.</returns> /// <returns>The item.</returns>
public T GetAt(int index) => _inner is IList<T> typed ? typed[index] : (T)_inner[index]; public object? GetAt(int index) => _inner[index];
public int IndexOf(object? item) => _inner.IndexOf(item); public int IndexOf(object? item) => _inner.IndexOf(item);
public static ItemsSourceView<T> GetOrCreate(IEnumerable<T>? items) public static ItemsSourceView GetOrCreate(IEnumerable? items)
{ {
if (items is ItemsSourceView<T> isv) if (items is ItemsSourceView isv)
{ {
return isv; return isv;
} }
@ -112,7 +114,7 @@ namespace Avalonia.Controls
} }
else else
{ {
return new ItemsSourceView<T>(items); return new ItemsSourceView(items);
} }
} }
@ -142,11 +144,6 @@ namespace Avalonia.Controls
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IEnumerator<T> GetEnumerator() => _inner is IList<T> typed ?
typed.GetEnumerator() : _inner.Cast<T>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
internal void AddListener(ICollectionChangedListener listener) internal void AddListener(ICollectionChangedListener listener)
{ {
if (_inner is INotifyCollectionChanged incc) if (_inner is INotifyCollectionChanged incc)
@ -183,11 +180,61 @@ namespace Avalonia.Controls
} }
} }
public class ItemsSourceView : ItemsSourceView<object?> public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
{ {
public ItemsSourceView(IEnumerable source) /// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public new static ItemsSourceView<T> Empty { get; } = new ItemsSourceView<T>(Array.Empty<T>());
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable<T> source)
: base(source) : base(source)
{ {
} }
private ItemsSourceView(IEnumerable source)
: base(source)
{
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
#pragma warning disable CS8603
public new T this[int index] => GetAt(index);
#pragma warning restore CS8603
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
[return: MaybeNull]
public new T GetAt(int index) => (T)_inner[index];
public IEnumerator<T> GetEnumerator() => _inner.Cast<T>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
{
if (items is ItemsSourceView<T> isv)
{
return isv;
}
else if (items is null)
{
return Empty;
}
else
{
return new ItemsSourceView<T>(items);
}
}
} }
} }

86
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -32,46 +32,10 @@ namespace Avalonia.Controls.Selection
Source = source; Source = source;
} }
public override IEnumerable<T>? Source public new IEnumerable<T>? Source
{ {
get => base.Source; get => base.Source as IEnumerable<T>;
set set => SetSource(value);
{
if (base.Source != value)
{
if (_operation is object)
{
throw new InvalidOperationException("Cannot change source while update is in progress.");
}
if (base.Source is object && value is object)
{
using var update = BatchUpdate();
update.Operation.SkipLostSelection = true;
Clear();
}
base.Source = value;
using (var update = BatchUpdate())
{
update.Operation.IsSourceUpdate = true;
if (_hasInitSelectedItem)
{
SelectedItem = _initSelectedItem;
_initSelectedItem = default;
_hasInitSelectedItem = false;
}
else
{
TrimInvalidSelections(update.Operation);
}
RaisePropertyChanged(nameof(Source));
}
}
}
} }
public bool SingleSelect public bool SingleSelect
@ -168,7 +132,7 @@ namespace Avalonia.Controls.Selection
IEnumerable? ISelectionModel.Source IEnumerable? ISelectionModel.Source
{ {
get => Source; get => Source;
set => Source = (IEnumerable<T>?)value; set => SetSource(value);
} }
object? ISelectionModel.SelectedItem object? ISelectionModel.SelectedItem
@ -298,6 +262,44 @@ namespace Avalonia.Controls.Selection
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
private void SetSource(IEnumerable? value)
{
if (base.Source != value)
{
if (_operation is object)
{
throw new InvalidOperationException("Cannot change source while update is in progress.");
}
if (base.Source is object && value is object)
{
using var update = BatchUpdate();
update.Operation.SkipLostSelection = true;
Clear();
}
base.Source = value;
using (var update = BatchUpdate())
{
update.Operation.IsSourceUpdate = true;
if (_hasInitSelectedItem)
{
SelectedItem = _initSelectedItem;
_initSelectedItem = default;
_hasInitSelectedItem = false;
}
else
{
TrimInvalidSelections(update.Operation);
}
RaisePropertyChanged(nameof(Source));
}
}
}
private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta) private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
{ {
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta)); IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
@ -511,7 +513,7 @@ namespace Avalonia.Controls.Selection
return default; return default;
} }
return ItemsView.GetAt(index); return ItemsView[index];
} }
private int CoerceIndex(int index) private int CoerceIndex(int index)
@ -710,7 +712,7 @@ namespace Avalonia.Controls.Selection
public int SelectedIndex { get; set; } public int SelectedIndex { get; set; }
public List<IndexRange>? SelectedRanges { get; set; } public List<IndexRange>? SelectedRanges { get; set; }
public List<IndexRange>? DeselectedRanges { get; set; } public List<IndexRange>? DeselectedRanges { get; set; }
public IReadOnlyList<T> DeselectedItems { get; set; } public IReadOnlyList<T>? DeselectedItems { get; set; }
} }
} }
} }

4
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -10,12 +10,12 @@ namespace Avalonia.Controls.Selection
{ {
public abstract class SelectionNodeBase<T> : ICollectionChangedListener public abstract class SelectionNodeBase<T> : ICollectionChangedListener
{ {
private IEnumerable<T>? _source; private IEnumerable? _source;
private bool _rangesEnabled; private bool _rangesEnabled;
private List<IndexRange>? _ranges; private List<IndexRange>? _ranges;
private int _collectionChanging; private int _collectionChanging;
public virtual IEnumerable<T>? Source protected IEnumerable? Source
{ {
get => _source; get => _source;
set set

8
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

@ -237,6 +237,14 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(1, raised); Assert.Equal(1, raised);
} }
[Fact]
public void Can_Assign_ValueType_Collection_To_SelectionModel_Of_Object()
{
var target = (ISelectionModel)new SelectionModel<object>();
target.Source = new[] { 1, 2, 3 };
}
} }
public class SelectedIndex public class SelectedIndex

Loading…
Cancel
Save