From ba7e8a20b5cd7e67f536dfe782e10972eda761ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Mar 2023 17:10:05 +0100 Subject: [PATCH] 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. --- .../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 +- .../Controls/ItemsRepeater.cs | 5 +- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 4 +- src/Avalonia.Controls/ItemCollection.cs | 114 ++++++++++++ src/Avalonia.Controls/ItemsControl.cs | 175 ++++++++++-------- src/Avalonia.Controls/ItemsSourceView.cs | 155 +++++++++------- .../Presenters/PanelContainerGenerator.cs | 15 -- .../Primitives/SelectingItemsControl.cs | 44 ++--- .../Selection/SelectionModel.cs | 4 +- .../SelectingItemsControlSelectionAdapter.cs | 4 +- src/Avalonia.Controls/VirtualizingPanel.cs | 20 +- .../ComboBoxTests.cs | 2 +- .../ItemsControlTests.cs | 60 +++--- .../ItemsSourceViewTests.cs | 39 ++++ .../Presenters/ItemsPresenterTests.cs | 2 +- .../TabControlTests.cs | 72 ++++--- .../TreeViewTests.cs | 2 +- .../VirtualizingCarouselPanelTests.cs | 2 +- .../VirtualizingStackPanelTests.cs | 13 +- .../CompiledBindingExtensionTests.cs | 5 +- 20 files changed, 452 insertions(+), 287 deletions(-) create mode 100644 src/Avalonia.Controls/ItemCollection.cs diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d624c9a07..54251417b3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); + fontComboBox.ItemsSource = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } } diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index 3d3d01e06e..499904deac 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -39,7 +39,10 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, + (o, v) => o.Items = v); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index b028a8f007..8211d1baf2 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(x => x.Items, + AvaloniaProperty.RegisterDirect( + nameof(Items), + x => x.Items, (x, v) => x.Items = v); /// diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs new file mode 100644 index 0000000000..120aef41dc --- /dev/null +++ b/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 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, + } + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9483f98881..56ba9c7183 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -34,8 +34,13 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ItemsProperty = - AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect( + 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 /// /// Defines the property. @@ -56,23 +61,23 @@ namespace Avalonia.Controls AvaloniaProperty.Register>(nameof(ItemsPanel), DefaultPanel); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ItemTemplateProperty = - AvaloniaProperty.Register(nameof(ItemTemplate)); + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly DirectProperty ItemsViewProperty = - AvaloniaProperty.RegisterDirect(nameof(ItemsView), o => o.ItemsView); + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); /// /// Defines the property /// public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); - + /// /// Defines the property. /// @@ -89,15 +94,14 @@ namespace Avalonia.Controls /// Gets or sets the to use for binding to the display member of each item. /// [AssignBinding] - [InheritDataTypeFromItems(nameof(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IBinding? DisplayMemberBinding { get => GetValue(DisplayMemberBindingProperty); set => SetValue(DisplayMemberBindingProperty, value); } - - private IEnumerable? _items = new AvaloniaList(); - private ItemsSourceView _itemsView; + + private readonly ItemCollection _items = new(); private int _itemCount; private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; @@ -110,9 +114,8 @@ namespace Avalonia.Controls /// public ItemsControl() { - _itemsView = ItemsSourceView.GetOrCreate(_items); - _itemsView.PostCollectionChanged += ItemsCollectionChanged; - UpdatePseudoClasses(0); + UpdatePseudoClasses(); + _items.CollectionChanged += OnItemsViewCollectionChanged; } /// @@ -129,10 +132,21 @@ namespace Avalonia.Controls /// Gets or sets the items to display. /// [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); + } + } } /// @@ -140,17 +154,24 @@ namespace Avalonia.Controls /// public ControlTheme? ItemContainerTheme { - get => GetValue(ItemContainerThemeProperty); + get => GetValue(ItemContainerThemeProperty); set => SetValue(ItemContainerThemeProperty, value); } /// - /// Gets the number of items in . + /// Gets the number of items being displayed by the . /// 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); + } + } } /// @@ -162,13 +183,44 @@ namespace Avalonia.Controls set => SetValue(ItemsPanelProperty, value); } + /// + /// Gets or sets a collection used to generate the content of the . + /// + /// + /// Since Avalonia 11, has both an property + /// and an property. The properties have the following differences: + /// + /// + /// is initialized with an empty collection and is a direct property, + /// meaning that it cannot be styled + /// 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. + /// + /// + /// 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: + /// + /// + /// You should use the property when you're assigning a collection of + /// item containers directly, for example adding a collection of s + /// directly to a . + /// You should use the property when you're assigning or + /// binding a collection of models which will be transformed by a data template. + /// + /// + public IEnumerable? ItemsSource + { + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + /// /// Gets or sets the data template used to display the items in the control. /// - [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 /// public Panel? ItemsPanelRoot => Presenter?.Panel; - /// - /// Gets a standardized view over . - /// - /// - /// The property may be an enumerable which does not implement - /// 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. - /// - 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 /// public bool AreHorizontalSnapPointsRegular { - get => GetValue(AreHorizontalSnapPointsRegularProperty); + get => GetValue(AreHorizontalSnapPointsRegularProperty); set => SetValue(AreHorizontalSnapPointsRegularProperty, value); } @@ -271,7 +298,7 @@ namespace Avalonia.Controls /// public bool AreVerticalSnapPointsRegular { - get => GetValue(AreVerticalSnapPointsRegularProperty); + get => GetValue(AreVerticalSnapPointsRegularProperty); set => SetValue(AreVerticalSnapPointsRegularProperty, value); } @@ -295,7 +322,7 @@ namespace Avalonia.Controls /// 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; } /// @@ -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()); - ItemCount = ItemsView.Count; - } - else if (change.Property == ItemCountProperty) + if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) { - UpdatePseudoClasses(change.GetNewValue()); - _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()); } else if (change.Property == ItemTemplateProperty) { @@ -524,14 +545,12 @@ namespace Avalonia.Controls /// /// Called when the event is - /// raised on . + /// raised on . /// /// The event sender. /// The event args. - 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; } /// @@ -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); } - /// - /// Given a collection of items, adds those that are controls to the logical children. - /// - /// The items. private void AddControlItemsToLogicalChildren(IEnumerable? items) { if (items is null) @@ -640,10 +657,6 @@ namespace Avalonia.Controls LogicalChildren.AddRange(toAdd); } - /// - /// Given a collection of items, removes those that are controls to from logical children. - /// - /// The items. 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( diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 416b909219..614b70d0ba 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -7,6 +7,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Controls.Utils; @@ -17,15 +18,16 @@ namespace Avalonia.Controls /// and an items control. /// public class ItemsSourceView : IReadOnlyList, + IList, INotifyCollectionChanged, ICollectionChangedListener { /// - /// Gets an empty + /// Gets an empty /// - public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); + public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private readonly IList _inner; + private 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. /// /// The data source. - 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 iObj => new List(iObj), - null => throw new ArgumentNullException(nameof(source)), - _ => new List(source.Cast()) - }; - } + private protected ItemsSourceView(IEnumerable source) => SetSource(source); /// /// Gets the number of items in the collection. /// - public int Count => Inner.Count; + public int Count => Source.Count; /// - /// Gets the inner collection. + /// Gets the source collection. /// - public IList Inner => _inner; + public IList Source => _source; /// /// Retrieves the item at the specified index. @@ -67,12 +56,20 @@ namespace Avalonia.Controls /// The item. 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(); + } + /// - /// Gets a value that indicates whether the items source can provide a unique key for each item. - /// - /// /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. - /// + /// internal bool HasKeyIndexMapping => false; /// @@ -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; - } - } - /// /// Retrieves the item at the specified index. /// /// The index. /// The item. - public object? GetAt(int index) => Inner[index]; - - /// - /// Determines the index of a specific item in the collection. - /// - /// The object to locate in the collection. - /// The index of value if found in the list; otherwise, -1. - 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); /// /// Gets or creates an for the specified enumerable. @@ -201,7 +173,8 @@ namespace Avalonia.Controls { return items switch { - ItemsSourceView isv => isv, + ItemsSourceView isvt => isvt, + ItemsSourceView isv => new ItemsSourceView(isv.Source), null => ItemsSourceView.Empty, _ => new ItemsSourceView(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); + /// - /// Retrieves the index of the item that has the specified unique identifier (key). + /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. /// - /// The index. - /// The key - /// - /// TODO: Not yet implemented in Avalonia. - /// 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 iObj => new List(iObj), + null => throw new ArgumentNullException(nameof(source)), + _ => new List(source.Cast()) + }; + + 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 : ItemsSourceView, IReadOnlyList @@ -306,7 +333,7 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public new T GetAt(int index) => (T)Inner[index]!; + public new T GetAt(int index) => (T)Source[index]!; public new IEnumerator 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(); } } diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index a2df8c3e5f..1cf0202772 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/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) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2ee32b0dda..7716e32e26 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/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; + } + /// /// Initializes static members of the class. /// @@ -229,7 +234,7 @@ namespace Avalonia.Controls.Primitives /// property /// [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; } - /// - 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(); - _selection.Source = newValue; - - if (newValue is null) - { - _selection.Clear(); - } - } else if (change.Property == SelectionModeProperty && _selection is object) { var newValue = change.GetNewValue(); @@ -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; + } + /// /// Called when is raised on /// . @@ -968,7 +968,7 @@ namespace Avalonia.Controls.Primitives /// The event args. 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(); } diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index d4c2b32974..68bad598d0 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -30,9 +30,9 @@ namespace Avalonia.Controls.Selection Source = source; } - public new IEnumerable? Source + public new IEnumerable? Source { - get => base.Source as IEnumerable; + get => base.Source; set => SetSource(value); } diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 3c1b1262ae..5f528e2c72 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/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; } } } diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs index 7780843eb5..a95d4f1ffa 100644 --- a/src/Avalonia.Controls/VirtualizingPanel.cs +++ b/src/Avalonia.Controls/VirtualizingPanel.cs @@ -34,7 +34,8 @@ namespace Avalonia.Controls /// /// Gets the items to display. /// - protected IReadOnlyList Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty; + protected IReadOnlyList Items => (IReadOnlyList?)ItemsControl?.ItemsView ?? + Array.Empty(); /// /// Gets the 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(); - oldValue.PostCollectionChanged -= OnItemsControlItemsChanged; - Refresh(); - newValue.PostCollectionChanged += OnItemsControlItemsChanged; - } - } - private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e); + OnItemsChanged(Items, e); } [DoesNotReturn] diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index e206d809d3..9e18285315 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/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(); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3aaf62f0bf..e7db6e3e67 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/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((_, __) => 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(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 { "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() { diff --git a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs index df842b21a7..faa143bb8e 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs @@ -38,6 +38,35 @@ namespace Avalonia.Controls.UnitTests Assert.Throws(() => ItemsSourceView.GetOrCreate(source)); } + [Fact] + public void Reassigning_Source_Unsubscribes_From_Previous_Source() + { + var source = new AvaloniaList(); + 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(); + 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 { 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); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 71f803fab7..c1ec66b2e9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/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((_, _) => result) }; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 0f72b2101a..e40ca44ba6 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/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(), target.GetLogicalChildren()); target.ApplyTemplate(); - Assert.Equal(items, target.GetLogicalChildren()); + Assert.Equal(target.Items.Cast(), 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((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().ToList(); Assert.Same(collection[0].Template, template); Assert.Same(collection[1].Template, template); Assert.Same(collection[2].Template, template); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2ca716fa8f..c397b0efb8 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1807,7 +1807,7 @@ namespace Avalonia.Controls.UnitTests return (TreeViewItem)c; } - private IList CreateTestTreeData() + private AvaloniaList CreateTestTreeData() { return new AvaloniaList { diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs index ea6b9367cf..721e8bde68 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs +++ b/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, }; diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index ba8e7242a1..c3dafb02db 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/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)itemsControl.Items!; + var items = (ObservableCollection)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)itemsControl.Items!; + var items = (ObservableCollection)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((_, _) => scroll), ItemsPanel = new FuncTemplate(() => target), }; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 27634b457b..c37f779df9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/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 ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + AvaloniaProperty.RegisterDirect( + nameof(Items), + x => x.Items, + (x, v) => x.Items = v); private IEnumerable _items; public IEnumerable Items