diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 2836937b79..b2663f3213 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/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 { /// - /// Represents a standardized view of the supported interactions between a given - /// or and its items. + /// Represents a standardized view of the supported interactions between a given ItemsSource + /// object and an control. /// - public class ItemsSourceView : INotifyCollectionChanged, IDisposable, IReadOnlyList + /// + /// Components written to work with ItemsRepeater should consume the + /// 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. + /// + public class ItemsSourceView : INotifyCollectionChanged, IDisposable { /// - /// Gets an empty + /// Gets an empty /// - public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); + public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private readonly IList _inner; + private protected readonly IList _inner; private INotifyCollectionChanged? _notifyCollectionChanged; /// /// Initializes a new instance of the ItemsSourceView class for the specified data source. /// /// The data source. - public ItemsSourceView(IEnumerable 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 enumerable) + else if (source is IEnumerable objectEnumerable) { - _inner = new List(enumerable); + _inner = new List(objectEnumerable); } else { - _inner = new List(source.Cast()); + _inner = new List(source.Cast()); } ListenToCollectionChanges(); @@ -75,7 +77,7 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public T this[int index] => GetAt(index); + public object? this[int index] => GetAt(index); /// /// Occurs when the collection has changed to indicate the reason for the change and which items changed. @@ -96,13 +98,13 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public T GetAt(int index) => _inner is IList typed ? typed[index] : (T)_inner[index]; + public object? GetAt(int index) => _inner[index]; public int IndexOf(object? item) => _inner.IndexOf(item); - public static ItemsSourceView GetOrCreate(IEnumerable? items) + public static ItemsSourceView GetOrCreate(IEnumerable? items) { - if (items is ItemsSourceView isv) + if (items is ItemsSourceView isv) { return isv; } @@ -112,7 +114,7 @@ namespace Avalonia.Controls } else { - return new ItemsSourceView(items); + return new ItemsSourceView(items); } } @@ -142,11 +144,6 @@ namespace Avalonia.Controls throw new NotImplementedException(); } - public IEnumerator GetEnumerator() => _inner is IList typed ? - typed.GetEnumerator() : _inner.Cast().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 + public class ItemsSourceView : ItemsSourceView, IReadOnlyList { - public ItemsSourceView(IEnumerable source) + /// + /// Gets an empty + /// + public new static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); + + /// + /// Initializes a new instance of the ItemsSourceView class for the specified data source. + /// + /// The data source. + public ItemsSourceView(IEnumerable source) : base(source) { } + + private ItemsSourceView(IEnumerable source) + : base(source) + { + } + + /// + /// Retrieves the item at the specified index. + /// + /// The index. + /// The item. +#pragma warning disable CS8603 + public new T this[int index] => GetAt(index); +#pragma warning restore CS8603 + + /// + /// Retrieves the item at the specified index. + /// + /// The index. + /// The item. + [return: MaybeNull] + public new T GetAt(int index) => (T)_inner[index]; + + public IEnumerator GetEnumerator() => _inner.Cast().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + + public static new ItemsSourceView GetOrCreate(IEnumerable? items) + { + if (items is ItemsSourceView isv) + { + return isv; + } + else if (items is null) + { + return Empty; + } + else + { + return new ItemsSourceView(items); + } + } } } diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 0427aeeb40..f34a358925 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -32,46 +32,10 @@ namespace Avalonia.Controls.Selection Source = source; } - public override IEnumerable? Source + public new IEnumerable? 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; + set => SetSource(value); } public bool SingleSelect @@ -168,7 +132,7 @@ namespace Avalonia.Controls.Selection IEnumerable? ISelectionModel.Source { get => Source; - set => Source = (IEnumerable?)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? SelectedRanges { get; set; } public List? DeselectedRanges { get; set; } - public IReadOnlyList DeselectedItems { get; set; } + public IReadOnlyList? DeselectedItems { get; set; } } } } diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 4796e8b9ca..ff3b8f43a8 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -10,12 +10,12 @@ namespace Avalonia.Controls.Selection { public abstract class SelectionNodeBase : ICollectionChangedListener { - private IEnumerable? _source; + private IEnumerable? _source; private bool _rangesEnabled; private List? _ranges; private int _collectionChanging; - public virtual IEnumerable? Source + protected IEnumerable? Source { get => _source; set diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 79b8da8f7b..345518e729 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/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(); + + target.Source = new[] { 1, 2, 3 }; + } } public class SelectedIndex