From 8bfe6b964543bfdab1282e175d9382d0973a01ff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 May 2023 16:12:35 +0200 Subject: [PATCH] Support heterogeneous item containers. --- src/Avalonia.Controls/ComboBox.cs | 11 +- .../Generators/ItemContainerGenerator.cs | 101 ++++++++++------ src/Avalonia.Controls/ItemsControl.cs | 58 +++++++++- src/Avalonia.Controls/ListBox.cs | 11 +- src/Avalonia.Controls/MenuBase.cs | 18 ++- src/Avalonia.Controls/MenuItem.cs | 18 ++- .../Presenters/PanelContainerGenerator.cs | 8 +- src/Avalonia.Controls/Primitives/TabStrip.cs | 11 +- src/Avalonia.Controls/TabControl.cs | 11 +- src/Avalonia.Controls/TreeView.cs | 11 +- src/Avalonia.Controls/TreeViewItem.cs | 8 +- .../VirtualizingCarouselPanel.cs | 97 ++++++++++------ .../VirtualizingStackPanel.cs | 109 +++++++++++------- .../ItemsControlTests.cs | 6 +- .../SelectingItemsControlTests_Multiple.cs | 16 ++- .../TreeViewTests.cs | 5 +- 16 files changed, 349 insertions(+), 150 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 1234b66383..7fca1b42ac 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -170,8 +170,15 @@ namespace Avalonia.Controls UpdateFlowDirection(); } - protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem(); - protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem; + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new ComboBoxItem(); + } + + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } /// protected override void OnKeyDown(KeyEventArgs e) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index b2c138599e..977552d5a3 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -9,34 +9,44 @@ namespace Avalonia.Controls.Generators /// When creating a container for an item from a , the following /// process should be followed: /// - /// - should first be called if the item is - /// derived from the class. If this method returns true then the - /// item itself should be used as the container. - /// - If returns false then - /// should be called to create a new container. + /// - should first be called to + /// determine whether the item needs a container. This method will return true if the item + /// should be wrapped in a container control, or false if the item itself can be used as a + /// container. + /// - If returns true then the + /// method should be called to create a new container, passing + /// the recycle key returned from . + /// - If the panel supports recycling and the recycle key is non-null then the recycle key + /// should be recorded for the container (e.g. in an attached property or the realized + /// container list). /// - method should be called for the /// container. /// - The container should then be added to the panel using /// /// - Finally, should be called. /// - /// NOTE: If in the first step above returns true - /// then the above steps should be carried out a single time; the first time the item is - /// displayed. Otherwise the steps should be carried out each time a new container is realized - /// for an item. + /// NOTE: If in the first step above + /// returns false then the above steps should be carried out a single time: the first time the + /// item is displayed. Otherwise the steps should be carried out each time a new container is + /// realized for an item. /// /// When unrealizing a container, the following process should be followed: /// - /// - If for the item returned true then the item - /// cannot be unrealized or recycled. + /// - If for the item returned false + /// then the item cannot be unrealized or recycled. /// - Otherwise, should be called for the container - /// - If recycling is supported then the container should be added to a recycle pool. - /// - It is assumed that recyclable containers will not be removed from the panel but instead - /// hidden from view using e.g. `container.IsVisible = false`. + /// - If recycling is supported by the panel and the container then the container should be + /// added to a recycle pool keyed on the recycle key returned from + /// . It is assumed that recycled + /// containers will not be removed from the panel but instead hidden from view using + /// e.g. `container.IsVisible = false`. + /// - If recycling is not supported then the container should be removed from the panel. /// /// When recycling an unrealized container, the following process should be followed: /// - /// - An element should be taken from the recycle pool. + /// - should be called to determine + /// whether the item needs a container, and if so, the recycle key. + /// - A container should be taken from the recycle pool keyed on the returned recycle key. /// - The container should be made visible. /// - method should be called for the /// container. @@ -54,28 +64,43 @@ namespace Avalonia.Controls.Generators internal ItemContainerGenerator(ItemsControl owner) => _owner = owner; /// - /// Creates a new container control. + /// Determines whether the specified item needs to be wrapped in a container control. /// - /// The newly created container control. - /// - /// Before calling this method, should be - /// called to determine whether the item itself should be used as a container. After - /// calling this method, should - /// be called to prepare the container to display the specified item. - /// - public Control CreateContainer() => _owner.CreateContainerForItemOverride(); + /// The item to display. + /// The index of the item. + /// + /// When the method returns, contains a key that can be used to locate a previously + /// recycled container of the correct type, or null if the item cannot be recycled. + /// + /// + /// true if the item needs a container; otherwise false if the item can itself be used + /// as a container. + /// + public bool NeedsContainer(object? item, int index, out object? recycleKey) => + _owner.NeedsContainerOverride(item, index, out recycleKey); /// - /// Determines whether the specified item is (or is eligible to be) its own container. + /// Creates a new container control. /// - /// The item. - /// true if the item is its own container, otherwise false. + /// The item to display. + /// The index of the item. + /// + /// The recycle key returned from + /// + /// The newly created container control. /// - /// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only - /// control items may be; the caller is responsible for checking if each item is a control - /// and calling this method before creating a new container. + /// Before calling this method, + /// should be called to determine whether the item itself should be used as a container. + /// After calling this method, + /// must be called to prepare the container to display the specified item. + /// + /// If the panel supports recycling then the returned recycle key should be stored alongside + /// the container and when container becomes eligible for recycling the container should + /// be placed in a recycle pool using this key. If the returned recycle key is null then + /// the container cannot be recycled. /// - public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container); + public Control CreateContainer(object? item, int index, object? recycleKey) + => _owner.CreateContainerForItemOverride(item, index, recycleKey); /// /// Prepares the specified element as the container for the corresponding item. @@ -84,10 +109,10 @@ namespace Avalonia.Controls.Generators /// The item to display. /// The index of the item to display. /// - /// If is true for an item, then this method - /// must only be called a single time, otherwise this method must be called after the - /// container is created, and each subsequent time the container is recycled to display a - /// new item. + /// If is false for an + /// item, then this method must only be called a single time; otherwise this method must + /// be called after the container is created, and each subsequent time the container is + /// recycled to display a new item. /// public void PrepareItemContainer(Control container, object? item, int index) => _owner.PrepareItemContainer(container, item, index); @@ -103,8 +128,8 @@ namespace Avalonia.Controls.Generators /// This method must be called when a container has been fully prepared and added /// to the logical and visual trees, but may be called before a layout pass has completed. /// It must be called regardless of the result of - /// but if that method returned true then - /// must be called only a single time. + /// but if that method returned + /// false then must be called only a single time. /// public void ItemContainerPrepared(Control container, object? item, int index) => _owner.ItemContainerPrepared(container, item, index); @@ -127,7 +152,7 @@ namespace Avalonia.Controls.Generators /// This method must be called when a container is unrealized. The container must have /// already have been removed from the virtualizing panel's list of realized containers before /// this method is called. This method must not be called if - /// returned true for the item. + /// returned false for the item. /// public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container); diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1c62de9bed..3e8e1f7a61 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -306,6 +306,12 @@ namespace Avalonia.Controls set => SetValue(AreVerticalSnapPointsRegularProperty, value); } + /// + /// Gets a default recycle key that can be used when an supports + /// a single container type. + /// + protected static object DefaultRecycleKey { get; } = new object(); + /// /// Returns the container for the item at the specified index. /// @@ -361,7 +367,10 @@ namespace Avalonia.Controls /// /// Creates or a container that can be used to display an item. /// - protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter(); + protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new ContentPresenter(); + } /// /// Prepares the specified element to display the specified item. @@ -494,11 +503,52 @@ namespace Avalonia.Controls } /// - /// Determines whether the specified item is (or is eligible to be) its own container. + /// Determines whether the specified item can be its own container. /// /// The item to check. - /// true if the item is (or is eligible to be) its own container; otherwise, false. - protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true; + /// The index of the item. + /// + /// When the method returns, contains a key that can be used to locate a previously + /// recycled container of the correct type, or null if the item cannot be recycled. + /// If the item is its own container then by definition it cannot be recycled, so + /// shoud be set to null. + /// + /// + /// true if the item needs a container; otherwise false if the item can itself be used + /// as a container. + /// + protected internal virtual bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } + + /// + /// A default implementation of + /// that returns true and sets the recycle key to if the item + /// is not a . + /// + /// The container type. + /// The item. + /// + /// When the method returns, contains if + /// is not of type ; otherwise null. + /// + /// + /// true if is of type ; otherwise false. + /// + protected bool NeedsContainer(object? item, out object? recycleKey) where T : Control + { + if (item is T) + { + recycleKey = null; + return false; + } + else + { + recycleKey = DefaultRecycleKey; + return true; + } + } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index e5f0b50555..2bdc37537a 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -108,8 +108,15 @@ namespace Avalonia.Controls /// public void UnselectAll() => Selection.Clear(); - protected internal override Control CreateContainerForItemOverride() => new ListBoxItem(); - protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem; + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new ListBoxItem(); + } + + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } /// protected override void OnGotFocus(GotFocusEventArgs e) diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index da7a36fa73..7fc804a338 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -133,8 +133,22 @@ namespace Avalonia.Controls /// bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); - protected internal override Control CreateContainerForItemOverride() => new MenuItem(); - protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator; + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new MenuItem(); + } + + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + if (item is MenuItem or Separator) + { + recycleKey = null; + return false; + } + + recycleKey = DefaultRecycleKey; + return true; + } /// protected override void OnKeyDown(KeyEventArgs e) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 72febcfedb..1bb53f90a8 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -339,8 +339,22 @@ namespace Avalonia.Controls /// void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); - protected internal override Control CreateContainerForItemOverride() => new MenuItem(); - protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator; + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new MenuItem(); + } + + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + if (item is MenuItem or Separator) + { + recycleKey = null; + return false; + } + + recycleKey = DefaultRecycleKey; + return true; + } protected override void OnPointerReleased(PointerReleasedEventArgs e) { diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index 5a6f9fc4f9..999c6db1bd 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs @@ -113,14 +113,14 @@ namespace Avalonia.Controls.Presenters var generator = itemsControl.ItemContainerGenerator; Control container; - if (item is Control c && generator.IsItemItsOwnContainer(c)) + if (generator.NeedsContainer(item, index, out var recycleKey)) { - container = c; - container.SetValue(ItemIsOwnContainerProperty, true); + container = generator.CreateContainer(item, index, recycleKey); } else { - container = generator.CreateContainer(); + container = (Control)item!; + container.SetValue(ItemIsOwnContainerProperty, true); } generator.PrepareItemContainer(container, item, index); diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 25cd1cd65b..ac64b827d5 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -16,8 +16,15 @@ namespace Avalonia.Controls.Primitives ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); } - protected internal override Control CreateContainerForItemOverride() => new TabStripItem(); - protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem; + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new TabStripItem(); + } + + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } /// protected override void OnGotFocus(GotFocusEventArgs e) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 310dd34382..0f06294d25 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -148,8 +148,15 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } - protected internal override Control CreateContainerForItemOverride() => new TabItem(); - protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem; + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new TabItem(); + } + + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index) { diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index e3a9a05951..149a95b6c7 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -485,8 +485,15 @@ namespace Avalonia.Controls return (false, null); } - protected internal override Control CreateContainerForItemOverride() => new TreeViewItem(); - protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem; + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new TreeViewItem(); + } + + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) { diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 70ffd218b1..958a381f1c 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -91,14 +91,14 @@ namespace Avalonia.Controls internal TreeView? TreeViewOwner => _treeView; - protected internal override Control CreateContainerForItemOverride() + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { - return EnsureTreeView().CreateContainerForItemOverride(); + return EnsureTreeView().CreateContainerForItemOverride(item, index, recycleKey); } - protected internal override bool IsItemItsOwnContainerOverride(Control item) + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { - return EnsureTreeView().IsItemItsOwnContainerOverride(item); + return EnsureTreeView().NeedsContainerOverride(item, index, out recycleKey); } protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index) diff --git a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs index 28d6a83309..958c19826d 100644 --- a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs +++ b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs @@ -15,13 +15,14 @@ namespace Avalonia.Controls /// public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable { - private static readonly AttachedProperty ItemIsOwnContainerProperty = - AvaloniaProperty.RegisterAttached("ItemIsOwnContainer"); + private static readonly AttachedProperty RecycleKeyProperty = + AvaloniaProperty.RegisterAttached("RecycleKey"); + private static readonly object s_itemIsItsOwnContainer = new object(); private Size _extent; private Vector _offset; private Size _viewport; - private Stack? _recyclePool; + private Dictionary>? _recyclePool; private Control? _realized; private int _realizedIndex = -1; private Control? _transitionFrom; @@ -172,7 +173,7 @@ namespace Avalonia.Controls return null; if (index == _realizedIndex) return _realized; - if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty)) + if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer) return c; return null; } @@ -246,10 +247,27 @@ namespace Avalonia.Controls private Control GetOrCreateElement(IReadOnlyList items, int index) { - return GetRealizedElement(index) ?? - GetItemIsOwnContainer(items, index) ?? - GetRecycledElement(items, index) ?? - CreateElement(items, index); + Debug.Assert(ItemContainerGenerator is not null); + + var e = GetRealizedElement(index); + + if (e is null) + { + var item = items[index]; + var generator = ItemContainerGenerator!; + + if (generator.NeedsContainer(item, index, out var recycleKey)) + { + e = GetRecycledElement(item, index, recycleKey) ?? + CreateElement(item, index, recycleKey); + } + else + { + e = GetItemAsOwnContainer(item, index); + } + } + + return e; } private Control? GetRealizedElement(int index) @@ -257,44 +275,37 @@ namespace Avalonia.Controls return _realizedIndex == index ? _realized : null; } - private Control? GetItemIsOwnContainer(IReadOnlyList items, int index) + private Control GetItemAsOwnContainer(object? item, int index) { Debug.Assert(ItemContainerGenerator is not null); - var item = items[index]; + var controlItem = (Control)item!; + var generator = ItemContainerGenerator!; - if (item is Control controlItem) + if (!controlItem.IsSet(RecycleKeyProperty)) { - var generator = ItemContainerGenerator; - - if (controlItem.IsSet(ItemIsOwnContainerProperty)) - { - controlItem.IsVisible = true; - return controlItem; - } - else if (generator.IsItemItsOwnContainer(controlItem)) - { - generator.PrepareItemContainer(controlItem, controlItem, index); - AddInternalChild(controlItem); - controlItem.SetValue(ItemIsOwnContainerProperty, true); - generator.ItemContainerPrepared(controlItem, item, index); - return controlItem; - } + generator.PrepareItemContainer(controlItem, controlItem, index); + AddInternalChild(controlItem); + controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer); + generator.ItemContainerPrepared(controlItem, item, index); } - return null; + controlItem.IsVisible = true; + return controlItem; } - private Control? GetRecycledElement(IReadOnlyList items, int index) + private Control? GetRecycledElement(object? item, int index, object? recycleKey) { Debug.Assert(ItemContainerGenerator is not null); - var generator = ItemContainerGenerator; - var item = items[index]; + if (recycleKey is null) + return null; + + var generator = ItemContainerGenerator!; - if (_recyclePool?.Count > 0) + if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0) { - var recycled = _recyclePool.Pop(); + var recycled = recyclePool.Pop(); recycled.IsVisible = true; generator.PrepareItemContainer(recycled, item, index); generator.ItemContainerPrepared(recycled, item, index); @@ -304,14 +315,14 @@ namespace Avalonia.Controls return null; } - private Control CreateElement(IReadOnlyList items, int index) + private Control CreateElement(object? item, int index, object? recycleKey) { Debug.Assert(ItemContainerGenerator is not null); - var generator = ItemContainerGenerator; - var item = items[index]; - var container = generator.CreateContainer(); + var generator = ItemContainerGenerator!; + var container = generator.CreateContainer(item, index, recycleKey); + container.SetValue(RecycleKeyProperty, recycleKey); generator.PrepareItemContainer(container, item, index); AddInternalChild(container); generator.ItemContainerPrepared(container, item, index); @@ -323,7 +334,10 @@ namespace Avalonia.Controls { Debug.Assert(ItemContainerGenerator is not null); - if (element.IsSet(ItemIsOwnContainerProperty)) + var recycleKey = element.GetValue(RecycleKeyProperty); + Debug.Assert(recycleKey is not null); + + if (recycleKey == s_itemIsItsOwnContainer) { element.IsVisible = false; } @@ -331,7 +345,14 @@ namespace Avalonia.Controls { ItemContainerGenerator.ClearItemContainer(element); _recyclePool ??= new(); - _recyclePool.Push(element); + + if (!_recyclePool.TryGetValue(recycleKey, out var pool)) + { + pool = new(); + _recyclePool.Add(recycleKey, pool); + } + + pool.Push(element); element.IsVisible = false; } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index e0768edfa4..ddeb42e560 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -52,10 +52,11 @@ namespace Avalonia.Controls nameof(VerticalSnapPointsChanged), RoutingStrategies.Bubble); - private static readonly AttachedProperty ItemIsOwnContainerProperty = - AvaloniaProperty.RegisterAttached("ItemIsOwnContainer"); + private static readonly AttachedProperty RecycleKeyProperty = + AvaloniaProperty.RegisterAttached("RecycleKey"); private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0); + private static readonly object s_itemIsItsOwnContainer = new object(); private readonly Action _recycleElement; private readonly Action _recycleElementOnItemRemoved; private readonly Action _updateElementIndex; @@ -68,7 +69,7 @@ namespace Avalonia.Controls private RealizedStackElements? _realizedElements; private ScrollViewer? _scrollViewer; private Rect _viewport = s_invalidViewport; - private Stack? _recyclePool; + private Dictionary>? _recyclePool; private Control? _unrealizedFocusedElement; private int _unrealizedFocusedIndex = -1; @@ -331,7 +332,7 @@ namespace Avalonia.Controls return null; if (_realizedElements?.GetElement(index) is { } realized) return realized; - if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty)) + if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer) return c; return null; } @@ -561,10 +562,26 @@ namespace Avalonia.Controls private Control GetOrCreateElement(IReadOnlyList items, int index) { - var e = GetRealizedElement(index) ?? - GetItemIsOwnContainer(items, index) ?? - GetRecycledElement(items, index) ?? - CreateElement(items, index); + Debug.Assert(ItemContainerGenerator is not null); + + var e = GetRealizedElement(index); + + if (e is null) + { + var item = items[index]; + var generator = ItemContainerGenerator!; + + if (generator.NeedsContainer(item, index, out var recycleKey)) + { + e = GetRecycledElement(item, index, recycleKey) ?? + CreateElement(item, index, recycleKey); + } + else + { + e = GetItemAsOwnContainer(item, index); + } + } + return e; } @@ -575,38 +592,33 @@ namespace Avalonia.Controls return _realizedElements?.GetElement(index); } - private Control? GetItemIsOwnContainer(IReadOnlyList items, int index) + private Control GetItemAsOwnContainer(object? item, int index) { - var item = items[index]; + Debug.Assert(ItemContainerGenerator is not null); - if (item is Control controlItem) - { - var generator = ItemContainerGenerator!; + var controlItem = (Control)item!; + var generator = ItemContainerGenerator!; - if (controlItem.IsSet(ItemIsOwnContainerProperty)) - { - controlItem.IsVisible = true; - return controlItem; - } - else if (generator.IsItemItsOwnContainer(controlItem)) - { - generator.PrepareItemContainer(controlItem, controlItem, index); - AddInternalChild(controlItem); - controlItem.SetValue(ItemIsOwnContainerProperty, true); - generator.ItemContainerPrepared(controlItem, item, index); - return controlItem; - } + if (!controlItem.IsSet(RecycleKeyProperty)) + { + generator.PrepareItemContainer(controlItem, controlItem, index); + AddInternalChild(controlItem); + controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer); + generator.ItemContainerPrepared(controlItem, item, index); } - return null; + controlItem.IsVisible = true; + return controlItem; } - private Control? GetRecycledElement(IReadOnlyList items, int index) + private Control? GetRecycledElement(object? item, int index, object? recycleKey) { Debug.Assert(ItemContainerGenerator is not null); + if (recycleKey is null) + return null; + var generator = ItemContainerGenerator!; - var item = items[index]; if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null) { @@ -617,9 +629,9 @@ namespace Avalonia.Controls return element; } - if (_recyclePool?.Count > 0) + if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0) { - var recycled = _recyclePool.Pop(); + var recycled = recyclePool.Pop(); recycled.IsVisible = true; generator.PrepareItemContainer(recycled, item, index); generator.ItemContainerPrepared(recycled, item, index); @@ -629,14 +641,14 @@ namespace Avalonia.Controls return null; } - private Control CreateElement(IReadOnlyList items, int index) + private Control CreateElement(object? item, int index, object? recycleKey) { Debug.Assert(ItemContainerGenerator is not null); var generator = ItemContainerGenerator!; - var item = items[index]; - var container = generator.CreateContainer(); + var container = generator.CreateContainer(item, index, recycleKey); + container.SetValue(RecycleKeyProperty, recycleKey); generator.PrepareItemContainer(container, item, index); AddInternalChild(container); generator.ItemContainerPrepared(container, item, index); @@ -650,7 +662,10 @@ namespace Avalonia.Controls _scrollViewer?.UnregisterAnchorCandidate(element); - if (element.IsSet(ItemIsOwnContainerProperty)) + var recycleKey = element.GetValue(RecycleKeyProperty); + Debug.Assert(recycleKey is not null); + + if (recycleKey == s_itemIsItsOwnContainer) { element.IsVisible = false; } @@ -663,8 +678,7 @@ namespace Avalonia.Controls else { ItemContainerGenerator!.ClearItemContainer(element); - _recyclePool ??= new(); - _recyclePool.Push(element); + PushToRecyclePool(recycleKey, element); element.IsVisible = false; } } @@ -673,19 +687,34 @@ namespace Avalonia.Controls { Debug.Assert(ItemContainerGenerator is not null); - if (element.IsSet(ItemIsOwnContainerProperty)) + var recycleKey = element.GetValue(RecycleKeyProperty); + Debug.Assert(recycleKey is not null); + + if (recycleKey == s_itemIsItsOwnContainer) { RemoveInternalChild(element); } else { ItemContainerGenerator!.ClearItemContainer(element); - _recyclePool ??= new(); - _recyclePool.Push(element); + PushToRecyclePool(recycleKey, element); element.IsVisible = false; } } + private void PushToRecyclePool(object recycleKey, Control element) + { + _recyclePool ??= new(); + + if (!_recyclePool.TryGetValue(recycleKey, out var pool)) + { + pool = new(); + _recyclePool.Add(recycleKey, pool); + } + + pool.Push(element); + } + private void UpdateElementIndex(Control element, int oldIndex, int newIndex) { Debug.Assert(ItemContainerGenerator is not null); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 5e741cdc1d..dec295169f 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -1034,14 +1034,14 @@ namespace Avalonia.Controls.UnitTests { Type IStyleable.StyleKey => typeof(ItemsControl); - protected internal override Control CreateContainerForItemOverride() + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { return new ContainerControl(); } - protected internal override bool IsItemItsOwnContainerOverride(Control item) + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { - return item is ContainerControl; + return NeedsContainer(item, out recycleKey); } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index daebc1e709..4f135d94ee 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1064,6 +1064,14 @@ namespace Avalonia.Controls.UnitTests.Primitives // Scroll selected item back into view. scroll.Offset = new(0, 0); + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + } + }; + Layout(target); // The selection should be preserved. @@ -1387,14 +1395,14 @@ namespace Avalonia.Controls.UnitTests.Primitives { Type IStyleable.StyleKey => typeof(TestSelector); - protected internal override bool IsItemItsOwnContainerOverride(Control item) + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { - return item is TestContainer; + return new TestContainer(); } - protected internal override Control CreateContainerForItemOverride() + protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { - return new TestContainer(); + return NeedsContainer(item, out recycleKey); } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index baf8ad5c0e..12792305e2 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1761,7 +1761,10 @@ namespace Avalonia.Controls.UnitTests private class DerivedTreeViewWithDerivedTreeViewItems : TreeView { - protected internal override Control CreateContainerForItemOverride() => new DerivedTreeViewItem(); + protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new DerivedTreeViewItem(); + } } private class DerivedTreeViewItem : TreeViewItem