Browse Source

Added ItemsControl.ItemsSource.

`ItemsControl` now works more like WPF, in that there are separate `Items` and `ItemsSource` properties. For backwards compatibility `Items` can still be set, though the setter is deprecated. `Items` needed to be changed from `IEnumerable` to `IList` though.
pull/10590/head
Steven Kirk 3 years ago
parent
commit
ba7e8a20b5
  1. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  2. 5
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  3. 4
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  4. 114
      src/Avalonia.Controls/ItemCollection.cs
  5. 175
      src/Avalonia.Controls/ItemsControl.cs
  6. 155
      src/Avalonia.Controls/ItemsSourceView.cs
  7. 15
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  8. 44
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  9. 4
      src/Avalonia.Controls/Selection/SelectionModel.cs
  10. 4
      src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
  11. 20
      src/Avalonia.Controls/VirtualizingPanel.cs
  12. 2
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  13. 60
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  14. 39
      tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs
  15. 2
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  16. 72
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  17. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  18. 2
      tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
  19. 13
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  20. 5
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.ItemsSource = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.SelectedIndex = 0;
}
}

5
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs

@ -39,7 +39,10 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v);
AvaloniaProperty.RegisterDirect<ItemsRepeater, IEnumerable?>(
nameof(Items),
o => o.Items,
(o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="Layout"/> property.

4
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -18,7 +18,9 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property
/// </summary>
public static readonly DirectProperty<MenuFlyout, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items,
AvaloniaProperty.RegisterDirect<MenuFlyout, IEnumerable?>(
nameof(Items),
x => x.Items,
(x, v) => x.Items = v);
/// <summary>

114
src/Avalonia.Controls/ItemCollection.cs

@ -0,0 +1,114 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Collections;
namespace Avalonia.Controls
{
public class ItemCollection : ItemsSourceView, IList
{
// Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique.
#pragma warning disable CA1825
private static readonly object?[] s_uninitialized = new object?[0];
#pragma warning restore CA1825
private Mode _mode;
internal ItemCollection()
: base(s_uninitialized)
{
}
public new object? this[int index]
{
get => base[index];
set => WritableSource[index] = value;
}
public bool IsReadOnly => _mode == Mode.ItemsSource;
internal event EventHandler? SourceChanged;
public int Add(object? value) => WritableSource.Add(value);
public void Clear() => WritableSource.Clear();
public void Insert(int index, object? value) => WritableSource.Insert(index, value);
public void RemoveAt(int index) => WritableSource.RemoveAt(index);
public bool Remove(object? value)
{
var c = Count;
WritableSource.Remove(value);
return Count < c;
}
int IList.Add(object? value) => Add(value);
void IList.Clear() => Clear();
void IList.Insert(int index, object? value) => Insert(index, value);
void IList.RemoveAt(int index) => RemoveAt(index);
private IList WritableSource
{
get
{
if (IsReadOnly)
ThrowIsItemsSource();
if (Source == s_uninitialized)
SetSource(CreateDefaultCollection());
return Source;
}
}
internal IList? GetItemsPropertyValue()
{
if (_mode == Mode.ObsoleteItemsSetter)
return Source == s_uninitialized ? null : Source;
return this;
}
internal void SetItems(IList? items)
{
_mode = Mode.ObsoleteItemsSetter;
SetSource(items ?? s_uninitialized);
}
internal void SetItemsSource(IEnumerable? value)
{
_mode = value is not null ? Mode.ItemsSource : Mode.Items;
SetSource(value ?? CreateDefaultCollection());
}
private new void SetSource(IEnumerable source)
{
var oldSource = Source;
base.SetSource(source);
if (oldSource.Count > 0)
RaiseCollectionChanged(new(NotifyCollectionChangedAction.Remove, oldSource, 0));
if (Source.Count > 0)
RaiseCollectionChanged(new(NotifyCollectionChangedAction.Add, Source, 0));
SourceChanged?.Invoke(this, EventArgs.Empty);
}
private static AvaloniaList<object?> CreateDefaultCollection()
{
return new() { ResetBehavior = ResetBehavior.Remove };
}
[DoesNotReturn]
private static void ThrowIsItemsSource()
{
throw new InvalidOperationException(
"Operation is not valid while ItemsSource is in use." +
"Access and modify elements with ItemsControl.ItemsSource instead.");
}
private enum Mode
{
Items,
ItemsSource,
ObsoleteItemsSetter,
}
}
}

175
src/Avalonia.Controls/ItemsControl.cs

@ -34,8 +34,13 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v);
public static readonly DirectProperty<ItemsControl, IList?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IList?>(
nameof(Items),
o => o.Items,
#pragma warning disable CS0618 // Type or member is obsolete
(o, v) => o.Items = v);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property.
@ -56,23 +61,23 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel);
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// Defines the <see cref="ItemsSource"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
AvaloniaProperty.Register<ItemsControl, IEnumerable?>(nameof(ItemsSource));
/// <summary>
/// Defines the <see cref="ItemsView"/> property.
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView);
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
@ -89,15 +94,14 @@ namespace Avalonia.Controls
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
[AssignBinding]
[InheritDataTypeFromItems(nameof(Items))]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IBinding? DisplayMemberBinding
{
get => GetValue(DisplayMemberBindingProperty);
set => SetValue(DisplayMemberBindingProperty, value);
}
private IEnumerable? _items = new AvaloniaList<object>();
private ItemsSourceView _itemsView;
private readonly ItemCollection _items = new();
private int _itemCount;
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
@ -110,9 +114,8 @@ namespace Avalonia.Controls
/// </summary>
public ItemsControl()
{
_itemsView = ItemsSourceView.GetOrCreate(_items);
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
UpdatePseudoClasses(0);
UpdatePseudoClasses();
_items.CollectionChanged += OnItemsViewCollectionChanged;
}
/// <summary>
@ -129,10 +132,21 @@ namespace Avalonia.Controls
/// Gets or sets the items to display.
/// </summary>
[Content]
public IEnumerable? Items
public IList? Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
get => _items.GetItemsPropertyValue();
[Obsolete("Use ItemsSource to set or bind items.")]
set
{
var oldItems = _items.GetItemsPropertyValue();
if (value != oldItems)
{
_items.SetItems(value);
RaisePropertyChanged(ItemsProperty, oldItems, value);
}
}
}
/// <summary>
@ -140,17 +154,24 @@ namespace Avalonia.Controls
/// </summary>
public ControlTheme? ItemContainerTheme
{
get => GetValue(ItemContainerThemeProperty);
get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value);
}
/// <summary>
/// Gets the number of items in <see cref="Items"/>.
/// Gets the number of items being displayed by the <see cref="ItemsControl"/>.
/// </summary>
public int ItemCount
{
get => _itemCount;
private set => SetAndRaise(ItemCountProperty, ref _itemCount, value);
private set
{
if (SetAndRaise(ItemCountProperty, ref _itemCount, value))
{
UpdatePseudoClasses();
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}
}
}
/// <summary>
@ -162,13 +183,44 @@ namespace Avalonia.Controls
set => SetValue(ItemsPanelProperty, value);
}
/// <summary>
/// Gets or sets a collection used to generate the content of the <see cref="ItemsControl"/>.
/// </summary>
/// <remarks>
/// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
/// and an <see cref="ItemsSource"/> property. The properties have the following differences:
///
/// <list type="bullet">
/// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
/// meaning that it cannot be styled </item>
/// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
/// is marked as the content property and will be used for items added via inline XAML.</item>
/// </list>
///
/// In Avalonia 11 the two properties can be used almost interchangeably but this will change
/// in a later version. In order to be ready for this change, follow the following guidance:
///
/// <list type="bullet">
/// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
/// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
/// directly to a <see cref="ListBox"/>.</item>
/// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
/// binding a collection of models which will be transformed by a data template.</item>
/// </list>
/// </remarks>
public IEnumerable? ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
[InheritDataTypeFromItems(nameof(Items))]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IDataTemplate? ItemTemplate
{
get => GetValue(ItemTemplateProperty);
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
@ -182,32 +234,7 @@ namespace Avalonia.Controls
/// </summary>
public Panel? ItemsPanelRoot => Presenter?.Panel;
/// <summary>
/// Gets a standardized view over <see cref="Items"/>.
/// </summary>
/// <remarks>
/// The <see cref="Items"/> property may be an enumerable which does not implement
/// <see cref="IList"/> or may be null. This view can be used to provide a standardized
/// view of the current items regardless of the type of the concrete collection, and
/// without having to deal with null values.
/// </remarks>
public ItemsSourceView ItemsView
{
get => _itemsView;
private set
{
if (ReferenceEquals(_itemsView, value))
return;
var oldValue = _itemsView;
RemoveControlItemsFromLogicalChildren(_itemsView);
_itemsView.PostCollectionChanged -= ItemsCollectionChanged;
_itemsView = value;
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
AddControlItemsToLogicalChildren(_itemsView);
RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView);
}
}
public ItemCollection ItemsView => _items;
private protected bool WrapFocus { get; set; }
@ -262,7 +289,7 @@ namespace Avalonia.Controls
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get => GetValue(AreHorizontalSnapPointsRegularProperty);
get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
@ -271,7 +298,7 @@ namespace Avalonia.Controls
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get => GetValue(AreVerticalSnapPointsRegularProperty);
get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
@ -295,7 +322,7 @@ namespace Avalonia.Controls
/// </returns>
public Control? ContainerFromItem(object item)
{
var index = ItemsView.IndexOf(item);
var index = _items.IndexOf(item);
return index >= 0 ? ContainerFromIndex(index) : null;
}
@ -319,7 +346,7 @@ namespace Avalonia.Controls
public object? ItemFromContainer(Control container)
{
var index = IndexFromContainer(container);
return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null;
return index >= 0 && index < _items.Count ? _items[index] : null;
}
/// <summary>
@ -389,7 +416,7 @@ namespace Avalonia.Controls
if (itemTemplate is ITreeDataTemplate treeTemplate)
{
if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding)
BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null);
BindingOperations.Apply(hic, ItemsSourceProperty, itemsBinding, null);
}
}
}
@ -485,19 +512,13 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
if (change.Property == ItemsProperty)
{
ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue<IEnumerable?>());
ItemCount = ItemsView.Count;
}
else if (change.Property == ItemCountProperty)
if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
UpdatePseudoClasses(change.GetNewValue<int>());
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
RefreshContainers();
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
else if (change.Property == ItemsSourceProperty)
{
RefreshContainers();
_items.SetItemsSource(change.GetNewValue<IEnumerable?>());
}
else if (change.Property == ItemTemplateProperty)
{
@ -524,14 +545,12 @@ namespace Avalonia.Controls
/// <summary>
/// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
/// raised on <see cref="Items"/>.
/// raised on <see cref="ItemsView"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
ItemCount = _itemsView.Count;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@ -542,6 +561,8 @@ namespace Avalonia.Controls
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
ItemCount = ItemsView.Count;
}
/// <summary>
@ -585,7 +606,7 @@ namespace Avalonia.Controls
{
var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme is not null &&
if (itemContainerTheme is not null &&
!container.IsSet(ThemeProperty) &&
((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
{
@ -616,10 +637,6 @@ namespace Avalonia.Controls
ClearContainerForItemOverride(container);
}
/// <summary>
/// Given a collection of items, adds those that are controls to the logical children.
/// </summary>
/// <param name="items">The items.</param>
private void AddControlItemsToLogicalChildren(IEnumerable? items)
{
if (items is null)
@ -640,10 +657,6 @@ namespace Avalonia.Controls
LogicalChildren.AddRange(toAdd);
}
/// <summary>
/// Given a collection of items, removes those that are controls to from logical children.
/// </summary>
/// <param name="items">The items.</param>
private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
{
if (items is null)
@ -681,10 +694,10 @@ namespace Avalonia.Controls
return _displayMemberItemTemplate;
}
private void UpdatePseudoClasses(int itemCount)
private void UpdatePseudoClasses()
{
PseudoClasses.Set(":empty", itemCount == 0);
PseudoClasses.Set(":singleitem", itemCount == 1);
PseudoClasses.Set(":empty", ItemCount == 0);
PseudoClasses.Set(":singleitem", ItemCount == 1);
}
protected static IInputElement? GetNextControl(

155
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;
@ -17,15 +18,16 @@ namespace Avalonia.Controls
/// and an items control.
/// </summary>
public class ItemsSourceView : IReadOnlyList<object?>,
IList,
INotifyCollectionChanged,
ICollectionChangedListener
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object?>());
private readonly IList _inner;
private IList _source;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged;
@ -35,30 +37,17 @@ namespace Avalonia.Controls
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
private protected ItemsSourceView(IEnumerable source)
{
_inner = source switch
{
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
}
private protected ItemsSourceView(IEnumerable source) => SetSource(source);
/// <summary>
/// Gets the number of items in the collection.
/// </summary>
public int Count => Inner.Count;
public int Count => Source.Count;
/// <summary>
/// Gets the inner collection.
/// Gets the source collection.
/// </summary>
public IList Inner => _inner;
public IList Source => _source;
/// <summary>
/// Retrieves the item at the specified index.
@ -67,12 +56,20 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public object? this[int index] => GetAt(index);
bool IList.IsFixedSize => false;
bool IList.IsReadOnly => true;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get => GetAt(index);
set => ThrowReadOnly();
}
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </remarks>
/// </summary>
internal bool HasKeyIndexMapping => false;
/// <summary>
@ -131,39 +128,14 @@ namespace Avalonia.Controls
}
}
private void AddListenerIfNecessary()
{
if (!_listening)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public object? GetAt(int index) => Inner[index];
/// <summary>
/// Determines the index of a specific item in the collection.
/// </summary>
/// <param name="item">The object to locate in the collection.</param>
/// <returns>The index of value if found in the list; otherwise, -1.</returns>
public int IndexOf(object? item) => Inner.IndexOf(item);
public object? GetAt(int index) => Source[index];
public bool Contains(object? item) => Source.Contains(item);
public int IndexOf(object? item) => Source.IndexOf(item);
/// <summary>
/// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
@ -201,7 +173,8 @@ namespace Avalonia.Controls
{
return items switch
{
ItemsSourceView<T> isv => isv,
ItemsSourceView<T> isvt => isvt,
ItemsSourceView isv => new ItemsSourceView<T>(isv.Source),
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
@ -236,7 +209,7 @@ namespace Avalonia.Controls
yield return o;
}
var inner = Inner;
var inner = Source;
return inner switch
{
@ -245,7 +218,7 @@ namespace Avalonia.Controls
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
@ -262,15 +235,69 @@ namespace Avalonia.Controls
_postCollectionChanged?.Invoke(this, e);
}
int IList.Add(object? value) => ThrowReadOnly();
void IList.Clear() => ThrowReadOnly();
void IList.Insert(int index, object? value) => ThrowReadOnly();
void IList.Remove(object? value) => ThrowReadOnly();
void IList.RemoveAt(int index) => ThrowReadOnly();
void ICollection.CopyTo(Array array, int index) => Source.CopyTo(array, index);
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal string KeyFromIndex(int index) => throw new NotImplementedException();
private protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
{
_preCollectionChanged?.Invoke(this, e);
_collectionChanged?.Invoke(this, e);
_postCollectionChanged?.Invoke(this, e);
}
[MemberNotNull(nameof(_source))]
private protected void SetSource(IEnumerable source)
{
if (_listening && _source is INotifyCollectionChanged inccOld)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
_source = source switch
{
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
if (_listening && _source is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
}
private void AddListenerIfNecessary()
{
if (!_listening)
{
if (_source is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_source is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
[DoesNotReturn]
private static int ThrowReadOnly() => throw new NotSupportedException("Collection is read-only.");
}
public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
@ -306,7 +333,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public new T GetAt(int index) => (T)Inner[index]!;
public new T GetAt(int index) => (T)Source[index]!;
public new IEnumerator<T> GetEnumerator()
{
@ -316,7 +343,7 @@ namespace Avalonia.Controls
yield return (T)o;
}
var inner = Inner;
var inner = Source;
return inner switch
{
@ -325,6 +352,6 @@ namespace Avalonia.Controls
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
}
}

15
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters
Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
_presenter = presenter;
_presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
_presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs);
@ -32,9 +31,7 @@ namespace Avalonia.Controls.Presenters
{
if (_presenter.ItemsControl is { } itemsControl)
{
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged;
ClearItemsControlLogicalChildren();
}
@ -43,18 +40,6 @@ namespace Avalonia.Controls.Presenters
internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs);
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsProperty)
{
if (e.OldValue is INotifyCollectionChanged inccOld)
inccOld.CollectionChanged -= OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs);
if (e.NewValue is INotifyCollectionChanged inccNew)
inccNew.CollectionChanged += OnItemsChanged;
}
}
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_presenter.Panel is null || _presenter.ItemsControl is null)

44
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -145,6 +145,11 @@ namespace Avalonia.Controls.Primitives
private BindingHelper? _bindingHelper;
private bool _isSelectionChangeActive;
public SelectingItemsControl()
{
ItemsView.SourceChanged += OnItemsViewSourceChanged;
}
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
/// </summary>
@ -229,7 +234,7 @@ namespace Avalonia.Controls.Primitives
/// <see cref="SelectedValue"/> property
/// </summary>
[AssignBinding]
[InheritDataTypeFromItems(nameof(Items))]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IBinding? SelectedValueBinding
{
get => GetValue(SelectedValueBindingProperty);
@ -322,7 +327,7 @@ namespace Avalonia.Controls.Primitives
}
else if (_selection != value)
{
if (value.Source != null && value.Source != Items)
if (value.Source != null && value.Source != ItemsView.Source)
{
throw new ArgumentException(
"The supplied ISelectionModel already has an assigned Source but this " +
@ -434,10 +439,9 @@ namespace Avalonia.Controls.Primitives
return null;
}
/// <inheritdoc />
protected override void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
private protected override void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender!, e);
base.OnItemsViewCollectionChanged(sender!, e);
if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
@ -547,7 +551,7 @@ namespace Avalonia.Controls.Primitives
if (_selection is object)
{
_selection.Source = Items;
_selection.Source = ItemsView.Source;
}
}
@ -635,16 +639,6 @@ namespace Avalonia.Controls.Primitives
{
AutoScrollToSelectedItemIfNecessary();
}
if (change.Property == ItemsProperty && _updateState is null && _selection is object)
{
var newValue = change.GetNewValue<IEnumerable?>();
_selection.Source = newValue;
if (newValue is null)
{
_selection.Clear();
}
}
else if (change.Property == SelectionModeProperty && _selection is object)
{
var newValue = change.GetNewValue<SelectionMode>();
@ -880,6 +874,12 @@ namespace Avalonia.Controls.Primitives
return false;
}
private void OnItemsViewSourceChanged(object? sender, EventArgs e)
{
if (_selection is not null && _updateState is null)
_selection.Source = ItemsView.Source;
}
/// <summary>
/// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on
/// <see cref="Selection"/>.
@ -968,7 +968,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="e">The event args.</param>
private void OnSelectionModelLostSelection(object? sender, EventArgs e)
{
if (AlwaysSelected && Items is object)
if (AlwaysSelected && ItemsView.Count > 0)
{
SelectedIndex = 0;
}
@ -998,14 +998,14 @@ namespace Avalonia.Controls.Primitives
}
}
private object FindItemWithValue(object? value)
private object? FindItemWithValue(object? value)
{
if (ItemCount == 0 || value is null)
{
return AvaloniaProperty.UnsetValue;
}
var items = Items;
var items = ItemsView;
var binding = SelectedValueBinding;
if (binding is null)
@ -1169,7 +1169,7 @@ namespace Avalonia.Controls.Primitives
{
if (_updateState is null)
{
model.Source = Items;
model.Source = ItemsView.Source;
}
model.PropertyChanged += OnSelectionModelPropertyChanged;
@ -1236,9 +1236,9 @@ namespace Avalonia.Controls.Primitives
SelectedItems = state.SelectedItems.Value;
}
Selection.Source = Items;
Selection.Source = ItemsView.Source;
if (Items is null)
if (ItemsView.Count == 0)
{
Selection.Clear();
}

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

@ -30,9 +30,9 @@ namespace Avalonia.Controls.Selection
Source = source;
}
public new IEnumerable<T>? Source
public new IEnumerable? Source
{
get => base.Source as IEnumerable<T>;
get => base.Source;
set => SetSource(value);
}

4
src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs

@ -144,13 +144,13 @@ namespace Avalonia.Controls.Utils
{
get
{
return SelectorControl?.Items;
return SelectorControl?.ItemsSource;
}
set
{
if (SelectorControl != null)
{
SelectorControl.Items = value;
SelectorControl.ItemsSource = value;
}
}
}

20
src/Avalonia.Controls/VirtualizingPanel.cs

@ -34,7 +34,8 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the items to display.
/// </summary>
protected IReadOnlyList<object?> Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty;
protected IReadOnlyList<object?> Items => (IReadOnlyList<object?>?)ItemsControl?.ItemsView ??
Array.Empty<object?>();
/// <summary>
/// Gets the <see cref="ItemsControl"/> that the panel is displaying items for.
@ -192,17 +193,13 @@ namespace Avalonia.Controls
throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl");
ItemsControl = itemsControl;
ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
ItemsControl.ItemsView.PostCollectionChanged += OnItemsControlItemsChanged;
}
internal void Detach()
{
var itemsControl = EnsureItemsControl();
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsControlItemsChanged;
ItemsControl = null;
Children.Clear();
}
@ -216,20 +213,9 @@ namespace Avalonia.Controls
return ItemsControl;
}
private protected virtual void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsViewProperty)
{
var (oldValue, newValue) = e.GetOldAndNewValue<ItemsSourceView>();
oldValue.PostCollectionChanged -= OnItemsControlItemsChanged;
Refresh();
newValue.PostCollectionChanged += OnItemsControlItemsChanged;
}
}
private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e);
OnItemsChanged(Items, e);
}
[DoesNotReturn]

2
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -257,7 +257,7 @@ namespace Avalonia.Controls.UnitTests
var target = new ComboBox
{
Template = GetTemplate(),
Items = items.Select(x => new ComboBoxItem { Content = x })
Items = items.Select(x => new ComboBoxItem { Content = x }).ToList(),
};
target.ApplyTemplate();

60
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -16,6 +16,19 @@ namespace Avalonia.Controls.UnitTests
{
public class ItemsControlTests
{
[Fact]
public void Setting_ItemsSource_Should_Populate_Items()
{
var target = new ItemsControl
{
Template = GetTemplate(),
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
ItemsSource = new[] { "foo", "bar" },
};
Assert.Equal(target.ItemsSource, target.Items);
}
[Fact]
public void Should_Use_ItemTemplate_To_Create_Control()
{
@ -153,7 +166,7 @@ namespace Avalonia.Controls.UnitTests
var child = new Control();
target.Template = GetTemplate();
target.Items = new[] { child };
target.Items.Add(child);
Assert.Equal(child.Parent, target);
Assert.Equal(child.GetLogicalParent(), target);
@ -206,11 +219,13 @@ namespace Avalonia.Controls.UnitTests
{
var target = new ItemsControl();
var child = new Control();
var items = new AvaloniaList<Control>(child);
target.Template = GetTemplate();
target.Items = items;
items.RemoveAt(0);
target.Items.Add(child);
Assert.Single(target.GetLogicalChildren());
target.Items.RemoveAt(0);
Assert.Null(child.Parent);
Assert.Null(child.GetLogicalParent());
@ -224,8 +239,11 @@ namespace Avalonia.Controls.UnitTests
var child = new Control();
target.Template = GetTemplate();
target.Items = new[] { child };
target.Items = null;
target.Items.Add(child);
Assert.Single(target.GetLogicalChildren());
target.Items.Clear();
Assert.Null(child.Parent);
Assert.Null(((ILogical)child).LogicalParent);
@ -253,7 +271,7 @@ namespace Avalonia.Controls.UnitTests
var child = new Control();
target.Template = GetTemplate();
target.Items = new[] { child };
target.Items.Add(child);
// Should appear both before and after applying template.
Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren());
@ -299,7 +317,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Setting_Items_Should_Fire_LogicalChildren_CollectionChanged()
public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged()
{
var target = new ItemsControl();
var child = new Control();
@ -311,7 +329,7 @@ namespace Avalonia.Controls.UnitTests
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
called = e.Action == NotifyCollectionChangedAction.Add;
target.Items = new[] { child };
target.Items.Add(child);
Assert.True(called);
}
@ -324,7 +342,7 @@ namespace Avalonia.Controls.UnitTests
var called = false;
target.Template = GetTemplate();
target.Items = new[] { child };
target.Items.Add(child);
target.ApplyTemplate();
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
@ -343,7 +361,7 @@ namespace Avalonia.Controls.UnitTests
var called = false;
target.Template = GetTemplate();
target.Items = new[] { child };
target.Items.Add(child);
target.ApplyTemplate();
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
@ -353,26 +371,6 @@ namespace Avalonia.Controls.UnitTests
Assert.True(called);
}
[Fact]
public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged()
{
var target = new ItemsControl();
var items = new AvaloniaList<string> { "Foo" };
var called = false;
target.Template = GetTemplate();
target.Items = items;
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
called = e.Action == NotifyCollectionChangedAction.Add;
items.Add("Bar");
Assert.True(called);
}
[Fact]
public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged()
{

39
tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs

@ -38,6 +38,35 @@ namespace Avalonia.Controls.UnitTests
Assert.Throws<ArgumentException>(() => ItemsSourceView.GetOrCreate(source));
}
[Fact]
public void Reassigning_Source_Unsubscribes_From_Previous_Source()
{
var source = new AvaloniaList<string>();
var target = new ReassignableItemsSourceView(source);
var debug = (INotifyCollectionChangedDebug)source;
target.CollectionChanged += (s, e) => { };
Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length);
target.SetSource(new string[0]);
Assert.Null(debug.GetCollectionChangedSubscribers());
}
[Fact]
public void Reassigning_Source_Subscribes_To_New_Source()
{
var source = new AvaloniaList<string>();
var target = new ReassignableItemsSourceView(new string[0]);
var debug = (INotifyCollectionChangedDebug)source;
target.CollectionChanged += (s, e) => { };
target.SetSource(source);
Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length);
}
private class InvalidCollection : INotifyCollectionChanged, IEnumerable<string>
{
public event NotifyCollectionChangedEventHandler CollectionChanged { add { } remove { } }
@ -52,5 +81,15 @@ namespace Avalonia.Controls.UnitTests
yield break;
}
}
private class ReassignableItemsSourceView : ItemsSourceView
{
public ReassignableItemsSourceView(IEnumerable source)
: base(source)
{
}
public new void SetSource(IEnumerable source) => base.SetSource(source);
}
}
}

2
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs

@ -139,7 +139,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
var itemsControl = new ItemsControl
{
Items = items,
ItemsSource = items,
Template = new FuncControlTemplate<ItemsControl>((_, _) => result)
};

72
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -71,27 +71,25 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Logical_Children_Should_Be_TabItems()
{
var items = new[]
{
new TabItem
{
Content = "foo"
},
new TabItem
{
Content = "bar"
},
};
var target = new TabControl
{
Template = TabControlTemplate(),
Items = items,
Items =
{
new TabItem
{
Content = "foo"
},
new TabItem
{
Content = "bar"
},
}
};
Assert.Equal(items, target.GetLogicalChildren());
Assert.Equal(target.Items.Cast<ILogical>(), target.GetLogicalChildren());
target.ApplyTemplate();
Assert.Equal(items, target.GetLogicalChildren());
Assert.Equal(target.Items.Cast<ILogical>(), target.GetLogicalChildren());
}
[Fact]
@ -207,26 +205,8 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate()
{
var collection = new[]
{
new TabItem
{
Name = "first",
Content = "foo",
},
new TabItem
{
Name = "second",
Content = "bar",
},
new TabItem
{
Name = "3rd",
Content = "barf",
},
};
var template = new FuncControlTemplate<TabItem>((x, __) => new Decorator());
TabControl target;
var root = new TestRoot
{
Styles =
@ -239,13 +219,31 @@ namespace Avalonia.Controls.UnitTests
}
}
},
Child = new TabControl
Child = (target = new TabControl
{
Template = TabControlTemplate(),
Items = collection,
}
Items =
{
new TabItem
{
Name = "first",
Content = "foo",
},
new TabItem
{
Name = "second",
Content = "bar",
},
new TabItem
{
Name = "3rd",
Content = "barf",
},
},
})
};
var collection = target.Items.Cast<TabItem>().ToList();
Assert.Same(collection[0].Template, template);
Assert.Same(collection[1].Template, template);
Assert.Same(collection[2].Template, template);

2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -1807,7 +1807,7 @@ namespace Avalonia.Controls.UnitTests
return (TreeViewItem)c;
}
private IList<Node> CreateTestTreeData()
private AvaloniaList<Node> CreateTestTreeData()
{
return new AvaloniaList<Node>
{

2
tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs

@ -218,7 +218,7 @@ namespace Avalonia.Controls.UnitTests
{
var carousel = new Carousel
{
Items = items,
ItemsSource = items,
Template = CarouselTemplate(),
PageTransition = transition,
};

13
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -9,15 +9,12 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
#nullable enable
namespace Avalonia.Controls.UnitTests
{
public class VirtualizingStackPanelTests
@ -99,7 +96,7 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var (target, _, itemsControl) = CreateTarget();
var items = (IList)itemsControl.Items!;
var items = (IList)itemsControl.ItemsSource!;
Assert.Equal(10, target.GetRealizedElements().Count);
@ -131,7 +128,7 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var (target, _, itemsControl) = CreateTarget();
var items = (IList)itemsControl.Items!;
var items = (IList)itemsControl.ItemsSource!;
Assert.Equal(10, target.GetRealizedElements().Count);
@ -161,7 +158,7 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var (target, _, itemsControl) = CreateTarget();
var items = (ObservableCollection<string>)itemsControl.Items!;
var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
Assert.Equal(10, target.GetRealizedElements().Count);
@ -190,7 +187,7 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var (target, _, itemsControl) = CreateTarget();
var items = (ObservableCollection<string>)itemsControl.Items!;
var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
Assert.Equal(10, target.GetRealizedElements().Count);
@ -473,7 +470,7 @@ namespace Avalonia.Controls.UnitTests
var itemsControl = new ItemsControl
{
Items = items,
ItemsSource = items,
Template = new FuncControlTemplate<ItemsControl>((_, _) => scroll),
ItemsPanel = new FuncTemplate<Panel>(() => target),
};

5
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -1991,7 +1991,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public class DataGridLikeControl : Control
{
public static readonly DirectProperty<DataGridLikeControl, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<DataGridLikeControl>(o => o.Items, (o, v) => o.Items = v);
AvaloniaProperty.RegisterDirect<DataGridLikeControl, IEnumerable?>(
nameof(Items),
x => x.Items,
(x, v) => x.Items = v);
private IEnumerable _items;
public IEnumerable Items

Loading…
Cancel
Save