Browse Source

Merge pull request #10590 from AvaloniaUI/feature/itemssource

Added ItemsControl.ItemsSource.
pull/10689/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
9413d93ee4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject
  2. 5
      .ncrunch/Generators.Sandbox.v3.ncrunchproject
  3. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  4. 2
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  5. 5
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  6. 4
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  7. 165
      src/Avalonia.Controls/ItemCollection.cs
  8. 211
      src/Avalonia.Controls/ItemsControl.cs
  9. 155
      src/Avalonia.Controls/ItemsSourceView.cs
  10. 15
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  11. 55
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  12. 27
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  13. 4
      src/Avalonia.Controls/Selection/SelectionModel.cs
  14. 4
      src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
  15. 20
      src/Avalonia.Controls/VirtualizingPanel.cs
  16. 43
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  17. 2
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  18. 118
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  19. 39
      tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs
  20. 2
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  21. 72
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  22. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  23. 2
      tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
  24. 13
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  25. 5
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

5
.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Generators.Sandbox.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

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

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

2
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@ -9,7 +9,7 @@ namespace Avalonia.Metadata;
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property, /// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
/// allowing the template to inherit the data type from the Items collection binding. /// allowing the template to inherit the data type from the Items collection binding.
/// </remarks> /// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class InheritDataTypeFromItemsAttribute : Attribute public sealed class InheritDataTypeFromItemsAttribute : Attribute
{ {
/// <summary> /// <summary>

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

@ -39,7 +39,10 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property. /// Defines the <see cref="Items"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable?> ItemsProperty = 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> /// <summary>
/// Defines the <see cref="Layout"/> property. /// Defines the <see cref="Layout"/> property.

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

@ -19,7 +19,9 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property /// Defines the <see cref="Items"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<MenuFlyout, IEnumerable?> ItemsProperty = 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); (x, v) => x.Items = v);
/// <summary> /// <summary>

165
src/Avalonia.Controls/ItemCollection.cs

@ -0,0 +1,165 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Collections;
namespace Avalonia.Controls
{
/// <summary>
/// Holds the list of items that constitute the content of an <see cref="ItemsControl"/>.
/// </summary>
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;
/// <summary>
/// Adds an item to the <see cref="ItemsControl"/>.
/// </summary>
/// <param name="value">The item to add to the collection.</param>
/// <returns>
/// The position into which the new element was inserted, or -1 to indicate that
/// the item was not inserted into the collection.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public int Add(object? value) => WritableSource.Add(value);
/// <summary>
/// Clears the collection and releases the references on all items currently in the
/// collection.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void Clear() => WritableSource.Clear();
/// <summary>
/// Inserts an element into the collection at the specified index.
/// </summary>
/// <param name="index">The zero-based index at which to insert the item.</param>
/// <param name="value">The item to insert.</param>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void Insert(int index, object? value) => WritableSource.Insert(index, value);
/// <summary>
/// Removes the item at the specified index of the collection or view.
/// </summary>
/// <param name="index">The zero-based index of the item to remove.</param>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void RemoveAt(int index) => WritableSource.RemoveAt(index);
/// <summary>
/// Removes the specified item reference from the collection or view.
/// </summary>
/// <param name="value">The object to remove.</param>
/// <returns>True if the item was removed; otherwise false.</returns>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
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)
{
if (_mode != Mode.ItemsSource && Count > 0)
throw new InvalidOperationException(
"Items collection must be empty before using ItemsSource.");
_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,
}
}
}

211
src/Avalonia.Controls/ItemsControl.cs

@ -34,8 +34,13 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Items"/> property. /// Defines the <see cref="Items"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty = public static readonly DirectProperty<ItemsControl, IList?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v); 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> /// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property. /// Defines the <see cref="ItemContainerTheme"/> property.
@ -56,23 +61,23 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel); AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel);
/// <summary> /// <summary>
/// Defines the <see cref="ItemTemplate"/> property. /// Defines the <see cref="ItemsSource"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate)); AvaloniaProperty.Register<ItemsControl, IEnumerable?>(nameof(ItemsSource));
/// <summary> /// <summary>
/// Defines the <see cref="ItemsView"/> property. /// Defines the <see cref="ItemTemplate"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty = public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView); AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary> /// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property /// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary> /// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty = public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding)); AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary> /// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property. /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary> /// </summary>
@ -89,15 +94,15 @@ namespace Avalonia.Controls
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item. /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary> /// </summary>
[AssignBinding] [AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))] [InheritDataTypeFromItems(nameof(Items))]
public IBinding? DisplayMemberBinding public IBinding? DisplayMemberBinding
{ {
get => GetValue(DisplayMemberBindingProperty); get => GetValue(DisplayMemberBindingProperty);
set => SetValue(DisplayMemberBindingProperty, value); set => SetValue(DisplayMemberBindingProperty, value);
} }
private IEnumerable? _items = new AvaloniaList<object>(); private readonly ItemCollection _items = new();
private ItemsSourceView _itemsView;
private int _itemCount; private int _itemCount;
private ItemContainerGenerator? _itemContainerGenerator; private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged; private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
@ -110,9 +115,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public ItemsControl() public ItemsControl()
{ {
_itemsView = ItemsSourceView.GetOrCreate(_items); UpdatePseudoClasses();
_itemsView.PostCollectionChanged += ItemsCollectionChanged; _items.CollectionChanged += OnItemsViewCollectionChanged;
UpdatePseudoClasses(0);
} }
/// <summary> /// <summary>
@ -128,11 +132,45 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets the items to display. /// Gets or sets the items to display.
/// </summary> /// </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"/>. Add the containers to the pre-existing list, do not
/// reassign the <see cref="Items"/> property via the setter or with a binding.</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>
[Content] [Content]
public IEnumerable? Items public IList? Items
{ {
get => _items; get => _items.GetItemsPropertyValue();
set => SetAndRaise(ItemsProperty, ref _items, value);
[Obsolete("Use ItemsSource to set or bind items.")]
set
{
var oldItems = _items.GetItemsPropertyValue();
if (value != oldItems)
{
_items.SetItems(value);
RaisePropertyChanged(ItemsProperty, oldItems, value);
}
}
} }
/// <summary> /// <summary>
@ -140,17 +178,24 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public ControlTheme? ItemContainerTheme public ControlTheme? ItemContainerTheme
{ {
get => GetValue(ItemContainerThemeProperty); get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value); set => SetValue(ItemContainerThemeProperty, value);
} }
/// <summary> /// <summary>
/// Gets the number of items in <see cref="Items"/>. /// Gets the number of items being displayed by the <see cref="ItemsControl"/>.
/// </summary> /// </summary>
public int ItemCount public int ItemCount
{ {
get => _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> /// <summary>
@ -162,13 +207,46 @@ namespace Avalonia.Controls
set => SetValue(ItemsPanelProperty, value); 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"/>. Add the containers to the pre-existing list, do not
/// reassign the <see cref="Items"/> property via the setter or with a binding.</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> /// <summary>
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
[InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))] [InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate public IDataTemplate? ItemTemplate
{ {
get => GetValue(ItemTemplateProperty); get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value); set => SetValue(ItemTemplateProperty, value);
} }
@ -183,31 +261,9 @@ namespace Avalonia.Controls
public Panel? ItemsPanelRoot => Presenter?.Panel; public Panel? ItemsPanelRoot => Presenter?.Panel;
/// <summary> /// <summary>
/// Gets a standardized view over <see cref="Items"/>. /// Gets a read-only view of the items in the <see cref="ItemsControl"/>.
/// </summary> /// </summary>
/// <remarks> public ItemsSourceView ItemsView => _items;
/// 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);
}
}
private protected bool WrapFocus { get; set; } private protected bool WrapFocus { get; set; }
@ -262,7 +318,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool AreHorizontalSnapPointsRegular public bool AreHorizontalSnapPointsRegular
{ {
get => GetValue(AreHorizontalSnapPointsRegularProperty); get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value); set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
} }
@ -271,7 +327,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool AreVerticalSnapPointsRegular public bool AreVerticalSnapPointsRegular
{ {
get => GetValue(AreVerticalSnapPointsRegularProperty); get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value); set => SetValue(AreVerticalSnapPointsRegularProperty, value);
} }
@ -295,7 +351,7 @@ namespace Avalonia.Controls
/// </returns> /// </returns>
public Control? ContainerFromItem(object item) public Control? ContainerFromItem(object item)
{ {
var index = ItemsView.IndexOf(item); var index = _items.IndexOf(item);
return index >= 0 ? ContainerFromIndex(index) : null; return index >= 0 ? ContainerFromIndex(index) : null;
} }
@ -319,7 +375,7 @@ namespace Avalonia.Controls
public object? ItemFromContainer(Control container) public object? ItemFromContainer(Control container)
{ {
var index = IndexFromContainer(container); var index = IndexFromContainer(container);
return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null; return index >= 0 && index < _items.Count ? _items[index] : null;
} }
/// <summary> /// <summary>
@ -478,19 +534,13 @@ namespace Avalonia.Controls
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == ItemsProperty) if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue<IEnumerable?>());
ItemCount = ItemsView.Count;
}
else if (change.Property == ItemCountProperty)
{ {
UpdatePseudoClasses(change.GetNewValue<int>()); RefreshContainers();
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
} }
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) else if (change.Property == ItemTemplateProperty)
{ {
@ -517,24 +567,27 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is /// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
/// raised on <see cref="Items"/>. /// raised on <see cref="ItemsView"/>.
/// </summary> /// </summary>
/// <param name="sender">The event sender.</param> /// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</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; if (!_items.IsReadOnly)
switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: switch (e.Action)
AddControlItemsToLogicalChildren(e.NewItems); {
break; case NotifyCollectionChangedAction.Add:
AddControlItemsToLogicalChildren(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
RemoveControlItemsFromLogicalChildren(e.OldItems); RemoveControlItemsFromLogicalChildren(e.OldItems);
break; break;
}
} }
ItemCount = ItemsView.Count;
} }
/// <summary> /// <summary>
@ -578,7 +631,7 @@ namespace Avalonia.Controls
{ {
var itemContainerTheme = ItemContainerTheme; var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme is not null && if (itemContainerTheme is not null &&
!container.IsSet(ThemeProperty) && !container.IsSet(ThemeProperty) &&
((IStyleable)container).StyleKey == itemContainerTheme.TargetType) ((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
{ {
@ -609,10 +662,6 @@ namespace Avalonia.Controls
ClearContainerForItemOverride(container); 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) private void AddControlItemsToLogicalChildren(IEnumerable? items)
{ {
if (items is null) if (items is null)
@ -633,10 +682,6 @@ namespace Avalonia.Controls
LogicalChildren.AddRange(toAdd); 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) private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
{ {
if (items is null) if (items is null)
@ -674,10 +719,10 @@ namespace Avalonia.Controls
return _displayMemberItemTemplate; return _displayMemberItemTemplate;
} }
private void UpdatePseudoClasses(int itemCount) private void UpdatePseudoClasses()
{ {
PseudoClasses.Set(":empty", itemCount == 0); PseudoClasses.Set(":empty", ItemCount == 0);
PseudoClasses.Set(":singleitem", itemCount == 1); PseudoClasses.Set(":singleitem", ItemCount == 1);
} }
protected static IInputElement? GetNextControl( protected static IInputElement? GetNextControl(

155
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;
@ -17,15 +18,16 @@ namespace Avalonia.Controls
/// and an items control. /// and an items control.
/// </summary> /// </summary>
public class ItemsSourceView : IReadOnlyList<object?>, public class ItemsSourceView : IReadOnlyList<object?>,
IList,
INotifyCollectionChanged, INotifyCollectionChanged,
ICollectionChangedListener ICollectionChangedListener
{ {
/// <summary> /// <summary>
/// Gets an empty <see cref="ItemsSourceView"/> /// Gets an empty <see cref="ItemsSourceView"/>
/// </summary> /// </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? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged; private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged; private NotifyCollectionChangedEventHandler? _postCollectionChanged;
@ -35,30 +37,17 @@ namespace Avalonia.Controls
/// 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>
private protected ItemsSourceView(IEnumerable source) private protected ItemsSourceView(IEnumerable source) => SetSource(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>())
};
}
/// <summary> /// <summary>
/// Gets the number of items in the collection. /// Gets the number of items in the collection.
/// </summary> /// </summary>
public int Count => Inner.Count; public int Count => Source.Count;
/// <summary> /// <summary>
/// Gets the inner collection. /// Gets the source collection.
/// </summary> /// </summary>
public IList Inner => _inner; public IList Source => _source;
/// <summary> /// <summary>
/// Retrieves the item at the specified index. /// Retrieves the item at the specified index.
@ -67,12 +56,20 @@ namespace Avalonia.Controls
/// <returns>The item.</returns> /// <returns>The item.</returns>
public object? this[int index] => GetAt(index); 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> /// <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. /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </remarks> /// </summary>
internal bool HasKeyIndexMapping => false; internal bool HasKeyIndexMapping => false;
/// <summary> /// <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> /// <summary>
/// Retrieves the item at the specified index. /// Retrieves the item at the specified index.
/// </summary> /// </summary>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
/// <returns>The item.</returns> /// <returns>The item.</returns>
public object? GetAt(int index) => Inner[index]; public object? GetAt(int index) => Source[index];
public bool Contains(object? item) => Source.Contains(item);
/// <summary> public int IndexOf(object? item) => Source.IndexOf(item);
/// 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);
/// <summary> /// <summary>
/// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable. /// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
@ -201,7 +173,8 @@ namespace Avalonia.Controls
{ {
return items switch return items switch
{ {
ItemsSourceView<T> isv => isv, ItemsSourceView<T> isvt => isvt,
ItemsSourceView isv => new ItemsSourceView<T>(isv.Source),
null => ItemsSourceView<T>.Empty, null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items) _ => new ItemsSourceView<T>(items)
}; };
@ -236,7 +209,7 @@ namespace Avalonia.Controls
yield return o; yield return o;
} }
var inner = Inner; var inner = Source;
return inner switch 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) void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{ {
@ -262,15 +235,69 @@ namespace Avalonia.Controls
_postCollectionChanged?.Invoke(this, e); _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> /// <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> /// </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(); 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> public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
@ -306,7 +333,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 new T GetAt(int index) => (T)Inner[index]!; public new T GetAt(int index) => (T)Source[index]!;
public new IEnumerator<T> GetEnumerator() public new IEnumerator<T> GetEnumerator()
{ {
@ -316,7 +343,7 @@ namespace Avalonia.Controls
yield return (T)o; yield return (T)o;
} }
var inner = Inner; var inner = Source;
return inner switch 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); Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
_presenter = presenter; _presenter = presenter;
_presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
_presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged; _presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged;
OnItemsChanged(null, CollectionUtils.ResetEventArgs); OnItemsChanged(null, CollectionUtils.ResetEventArgs);
@ -32,9 +31,7 @@ namespace Avalonia.Controls.Presenters
{ {
if (_presenter.ItemsControl is { } itemsControl) if (_presenter.ItemsControl is { } itemsControl)
{ {
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged; itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged;
ClearItemsControlLogicalChildren(); ClearItemsControlLogicalChildren();
} }
@ -43,18 +40,6 @@ namespace Avalonia.Controls.Presenters
internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs); 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) private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
if (_presenter.Panel is null || _presenter.ItemsControl is null) if (_presenter.Panel is null || _presenter.ItemsControl is null)

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

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

27
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -5,6 +5,7 @@ using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
namespace Avalonia.Controls.Selection namespace Avalonia.Controls.Selection
{ {
@ -13,6 +14,7 @@ namespace Avalonia.Controls.Selection
private IList? _writableSelectedItems; private IList? _writableSelectedItems;
private int _ignoreModelChanges; private int _ignoreModelChanges;
private bool _ignoreSelectedItemsChanges; private bool _ignoreSelectedItemsChanges;
private bool _skipSyncFromSelectedItems;
private bool _isResetting; private bool _isResetting;
public InternalSelectionModel() public InternalSelectionModel()
@ -60,6 +62,29 @@ namespace Avalonia.Controls.Selection
} }
} }
internal void Update(IEnumerable? source, Optional<IList?> selectedItems)
{
var previousSource = Source;
var previousWritableSelectedItems = _writableSelectedItems;
try
{
_skipSyncFromSelectedItems = true;
SetSource(source);
if (selectedItems.HasValue)
WritableSelectedItems = selectedItems.Value;
}
finally
{
_skipSyncFromSelectedItems = false;
}
// We skipped the sync from WritableSelectedItems before; do it now that both
// the source and WritableSelectedItems are updated.
if (previousSource != Source || previousWritableSelectedItems != _writableSelectedItems)
SyncFromSelectedItems();
}
private protected override void SetSource(IEnumerable? value) private protected override void SetSource(IEnumerable? value)
{ {
if (Source == value) if (Source == value)
@ -121,7 +146,7 @@ namespace Avalonia.Controls.Selection
private void SyncFromSelectedItems() private void SyncFromSelectedItems()
{ {
if (Source is null || _writableSelectedItems is null) if (_skipSyncFromSelectedItems || Source is null || _writableSelectedItems is null)
{ {
return; return;
} }

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

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

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

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

20
src/Avalonia.Controls/VirtualizingPanel.cs

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

43
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -73,27 +73,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
// Infer data type from collection binding on a control that displays items. // Infer data type from collection binding on a control that displays items.
var property = context.ParentNodes().OfType<XamlPropertyAssignmentNode>().FirstOrDefault(); var property = context.ParentNodes().OfType<XamlPropertyAssignmentNode>().FirstOrDefault();
var attributeType = context.GetAvaloniaTypes().InheritDataTypeFromItemsAttribute; var attributeType = context.GetAvaloniaTypes().InheritDataTypeFromItemsAttribute;
var attribute = property?.Property?.GetClrProperty().CustomAttributes var attributes = property?.Property?.GetClrProperty().CustomAttributes
.FirstOrDefault(a => a.Type == attributeType); .Where(a => a.Type == attributeType).ToList();
if (attribute is not null) if (attributes?.Count > 0)
{ {
var propertyName = (string)attribute.Parameters.First(); foreach (var attribute in attributes)
XamlAstConstructableObjectNode parentObject;
if (attribute.Properties.TryGetValue("AncestorType", out var type)
&& type is IXamlType xamlType)
{
parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>()
.FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName);
}
else
{ {
parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault(); var propertyName = (string)attribute.Parameters.First();
} XamlAstConstructableObjectNode parentObject;
if (attribute.Properties.TryGetValue("AncestorType", out var type)
if (parentObject != null) && type is IXamlType xamlType)
{ {
inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName); parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>()
.FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName);
}
else
{
parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault();
}
if (parentObject != null)
{
inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName);
if (inferredDataContextTypeNode != null)
break;
}
} }
} }

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

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

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

@ -16,6 +16,46 @@ namespace Avalonia.Controls.UnitTests
{ {
public class ItemsControlTests 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.NotSame(target.ItemsSource, target.Items);
Assert.Equal(target.ItemsSource, target.Items);
}
[Fact]
public void Cannot_Set_ItemsSource_With_Items_Present()
{
var target = new ItemsControl
{
Template = GetTemplate(),
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
Items = { "foo", "bar" },
};
Assert.Throws<InvalidOperationException>(() => target.ItemsSource = new[] { "baz" });
}
[Fact]
public void Cannot_Modify_Items_When_ItemsSource_Set()
{
var target = new ItemsControl
{
Template = GetTemplate(),
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
ItemsSource = Array.Empty<string>(),
};
Assert.Throws<InvalidOperationException>(() => target.Items.Add("foo"));
}
[Fact] [Fact]
public void Should_Use_ItemTemplate_To_Create_Control() public void Should_Use_ItemTemplate_To_Create_Control()
{ {
@ -153,7 +193,7 @@ namespace Avalonia.Controls.UnitTests
var child = new Control(); var child = new Control();
target.Template = GetTemplate(); target.Template = GetTemplate();
target.Items = new[] { child }; target.Items.Add(child);
Assert.Equal(child.Parent, target); Assert.Equal(child.Parent, target);
Assert.Equal(child.GetLogicalParent(), target); Assert.Equal(child.GetLogicalParent(), target);
@ -206,11 +246,13 @@ namespace Avalonia.Controls.UnitTests
{ {
var target = new ItemsControl(); var target = new ItemsControl();
var child = new Control(); var child = new Control();
var items = new AvaloniaList<Control>(child);
target.Template = GetTemplate(); target.Template = GetTemplate();
target.Items = items; target.Items.Add(child);
items.RemoveAt(0);
Assert.Single(target.GetLogicalChildren());
target.Items.RemoveAt(0);
Assert.Null(child.Parent); Assert.Null(child.Parent);
Assert.Null(child.GetLogicalParent()); Assert.Null(child.GetLogicalParent());
@ -224,13 +266,47 @@ namespace Avalonia.Controls.UnitTests
var child = new Control(); var child = new Control();
target.Template = GetTemplate(); target.Template = GetTemplate();
target.Items = new[] { child }; target.Items.Add(child);
target.Items = null;
Assert.Single(target.GetLogicalChildren());
target.Items.Clear();
Assert.Null(child.Parent); Assert.Null(child.Parent);
Assert.Null(((ILogical)child).LogicalParent); Assert.Null(((ILogical)child).LogicalParent);
} }
[Fact]
public void Assigning_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate()
{
var target = new ItemsControl();
var child = new Control();
var called = false;
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
var list = new AvaloniaList<Control>(new[] { child });
target.ItemsSource = list;
Assert.False(called);
}
[Fact]
public void Changing_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate()
{
var target = new ItemsControl();
var child = new Control();
var called = false;
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
var list = new AvaloniaList<Control>();
target.ItemsSource = list;
list.Add(child);
Assert.False(called);
}
[Fact] [Fact]
public void Clearing_Items_Should_Clear_Child_Controls_Parent() public void Clearing_Items_Should_Clear_Child_Controls_Parent()
{ {
@ -253,7 +329,7 @@ namespace Avalonia.Controls.UnitTests
var child = new Control(); var child = new Control();
target.Template = GetTemplate(); target.Template = GetTemplate();
target.Items = new[] { child }; target.Items.Add(child);
// Should appear both before and after applying template. // Should appear both before and after applying template.
Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren()); Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren());
@ -299,7 +375,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Setting_Items_Should_Fire_LogicalChildren_CollectionChanged() public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged()
{ {
var target = new ItemsControl(); var target = new ItemsControl();
var child = new Control(); var child = new Control();
@ -311,7 +387,7 @@ namespace Avalonia.Controls.UnitTests
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
called = e.Action == NotifyCollectionChangedAction.Add; called = e.Action == NotifyCollectionChangedAction.Add;
target.Items = new[] { child }; target.Items.Add(child);
Assert.True(called); Assert.True(called);
} }
@ -324,7 +400,7 @@ namespace Avalonia.Controls.UnitTests
var called = false; var called = false;
target.Template = GetTemplate(); target.Template = GetTemplate();
target.Items = new[] { child }; target.Items.Add(child);
target.ApplyTemplate(); target.ApplyTemplate();
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
@ -343,7 +419,7 @@ namespace Avalonia.Controls.UnitTests
var called = false; var called = false;
target.Template = GetTemplate(); target.Template = GetTemplate();
target.Items = new[] { child }; target.Items.Add(child);
target.ApplyTemplate(); target.ApplyTemplate();
((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
@ -353,26 +429,6 @@ namespace Avalonia.Controls.UnitTests
Assert.True(called); 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] [Fact]
public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged() 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)); 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> private class InvalidCollection : INotifyCollectionChanged, IEnumerable<string>
{ {
public event NotifyCollectionChangedEventHandler CollectionChanged { add { } remove { } } public event NotifyCollectionChangedEventHandler CollectionChanged { add { } remove { } }
@ -52,5 +81,15 @@ namespace Avalonia.Controls.UnitTests
yield break; 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 var itemsControl = new ItemsControl
{ {
Items = items, ItemsSource = items,
Template = new FuncControlTemplate<ItemsControl>((_, _) => result) Template = new FuncControlTemplate<ItemsControl>((_, _) => result)
}; };

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

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

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

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

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

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

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

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

Loading…
Cancel
Save