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.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Utils;
@ -15,29 +16,30 @@ using Avalonia.Controls.Utils;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a standardized view of the supported interactions between a given
/// <see cref="ItemsControl"/> or <see cref="ItemsRepeater"/> and its items.
/// Represents a standardized view of the supported interactions between a given ItemsSource
/// object and an <see cref="ItemsRepeater"/> control.
/// </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>
/// Gets an empty <see cref="ItemsSourceView{T}"/>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </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;
/// <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)
: this((IEnumerable)source)
{
}
private protected ItemsSourceView(IEnumerable source)
public ItemsSourceView(IEnumerable source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
@ -45,13 +47,13 @@ namespace Avalonia.Controls
{
_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
{
_inner = new List<object?>(source.Cast<object?>());
_inner = new List<object>(source.Cast<object>());
}
ListenToCollectionChanges();
@ -75,7 +77,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public T this[int index] => GetAt(index);
public object? this[int index] => GetAt(index);
/// <summary>
/// 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>
/// <param name="index">The index.</param>
/// <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 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;
}
@ -112,7 +114,7 @@ namespace Avalonia.Controls
}
else
{
return new ItemsSourceView<T>(items);
return new ItemsSourceView(items);
}
}
@ -142,11 +144,6 @@ namespace Avalonia.Controls
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)
{
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)
{
}
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;
}
public override IEnumerable<T>? Source
public new IEnumerable<T>? Source
{
get => base.Source;
set
{
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));
}
}
}
get => base.Source as IEnumerable<T>;
set => SetSource(value);
}
public bool SingleSelect
@ -168,7 +132,7 @@ namespace Avalonia.Controls.Selection
IEnumerable? ISelectionModel.Source
{
get => Source;
set => Source = (IEnumerable<T>?)value;
set => SetSource(value);
}
object? ISelectionModel.SelectedItem
@ -298,6 +262,44 @@ namespace Avalonia.Controls.Selection
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)
{
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
@ -511,7 +513,7 @@ namespace Avalonia.Controls.Selection
return default;
}
return ItemsView.GetAt(index);
return ItemsView[index];
}
private int CoerceIndex(int index)
@ -710,7 +712,7 @@ namespace Avalonia.Controls.Selection
public int SelectedIndex { get; set; }
public List<IndexRange>? SelectedRanges { 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
{
private IEnumerable<T>? _source;
private IEnumerable? _source;
private bool _rangesEnabled;
private List<IndexRange>? _ranges;
private int _collectionChanging;
public virtual IEnumerable<T>? Source
protected IEnumerable? Source
{
get => _source;
set

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

@ -237,6 +237,14 @@ namespace Avalonia.Controls.UnitTests.Selection
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

Loading…
Cancel
Save