浏览代码

Support heterogeneous item containers.

Steven Kirk 2 年之前
父节点
当前提交
8bfe6b9645

+ 9 - 2
src/Avalonia.Controls/ComboBox.cs

@@ -170,8 +170,15 @@ namespace Avalonia.Controls
             UpdateFlowDirection();
             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<ComboBoxItem>(item, out recycleKey);
+        }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         protected override void OnKeyDown(KeyEventArgs e)

+ 63 - 38
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@@ -9,34 +9,44 @@ namespace Avalonia.Controls.Generators
     /// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
     /// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
     /// process should be followed:
     /// process should be followed:
     ///
     ///
-    /// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
-    ///   derived from the <see cref="Control"/> class. If this method returns true then the
-    ///   item itself should be used as the container.
-    /// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then
-    ///   <see cref="CreateContainer"/> should be called to create a new container.
+    /// - <see cref="NeedsContainer(object, int, out object?)"/> 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 <see cref="NeedsContainer(object, int, out object?)"/> returns true then the
+    ///   <see cref="CreateContainer"/> method should be called to create a new container, passing
+    ///   the recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>.
+    /// - 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).
     /// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
     /// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
     ///   container.
     ///   container.
     /// - The container should then be added to the panel using 
     /// - The container should then be added to the panel using 
     ///   <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
     ///   <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
     /// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
     /// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
     /// 
     /// 
-    /// NOTE: If <see cref="IsItemItsOwnContainer(Control)"/> 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 <see cref="NeedsContainer(object, int, out object?)"/> 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:
     /// When unrealizing a container, the following process should be followed:
     /// 
     /// 
-    /// - If <see cref="IsItemItsOwnContainer(Control)"/> for the item returned true then the item
-    ///   cannot be unrealized or recycled.
+    /// - If <see cref="NeedsContainer(object, int, out object?)"/> for the item returned false
+    ///   then the item cannot be unrealized or recycled.
     /// - Otherwise, <see cref="ClearItemContainer(Control)"/> should be called for the container
     /// - Otherwise, <see cref="ClearItemContainer(Control)"/> 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 
+    ///   <see cref="NeedsContainer(object, int, out object?)"/>. 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:
     /// When recycling an unrealized container, the following process should be followed:
     /// 
     /// 
-    /// - An element should be taken from the recycle pool.
+    /// - <see cref="NeedsContainer(object, int, out object?)"/> 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.
     /// - The container should be made visible.
     /// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
     /// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
     ///   container.
     ///   container.
@@ -54,28 +64,43 @@ namespace Avalonia.Controls.Generators
         internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
         internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
 
 
         /// <summary>
         /// <summary>
-        /// Creates a new container control.
+        /// Determines whether the specified item needs to be wrapped in a container control.
         /// </summary>
         /// </summary>
-        /// <returns>The newly created container control.</returns>
-        /// <remarks>
-        /// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
-        /// called to determine whether the item itself should be used as a container. After
-        /// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
-        /// be called to prepare the container to display the specified item.
-        /// </remarks>
-        public Control CreateContainer() => _owner.CreateContainerForItemOverride();
+        /// <param name="item">The item to display.</param>
+        /// <param name="index">The index of the item.</param>
+        /// <param name="recycleKey">
+        /// 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.
+        /// </param>
+        /// <returns>
+        /// true if the item needs a container; otherwise false if the item can itself be used
+        /// as a container.
+        /// </returns>
+        public bool NeedsContainer(object? item, int index, out object? recycleKey) =>
+            _owner.NeedsContainerOverride(item, index, out recycleKey);
 
 
         /// <summary>
         /// <summary>
-        /// Determines whether the specified item is (or is eligible to be) its own container.
+        /// Creates a new container control.
         /// </summary>
         /// </summary>
-        /// <param name="container">The item.</param>
-        /// <returns>true if the item is its own container, otherwise false.</returns>
+        /// <param name="item">The item to display.</param>
+        /// <param name="index">The index of the item.</param>
+        /// <param name="recycleKey">
+        /// The recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>
+        /// </param>
+        /// <returns>The newly created container control.</returns>
         /// <remarks>
         /// <remarks>
-        /// 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, <see cref="NeedsContainer(object, int, out object?)"/>
+        /// should be called to determine whether the item itself should be used as a container.
+        /// After calling this method, <see cref="PrepareItemContainer(Control, object, int)"/>
+        /// 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.
         /// </remarks>
         /// </remarks>
-        public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
+        public Control CreateContainer(object? item, int index, object? recycleKey) 
+            => _owner.CreateContainerForItemOverride(item, index, recycleKey);
 
 
         /// <summary>
         /// <summary>
         /// Prepares the specified element as the container for the corresponding item.
         /// Prepares the specified element as the container for the corresponding item.
@@ -84,10 +109,10 @@ namespace Avalonia.Controls.Generators
         /// <param name="item">The item to display.</param>
         /// <param name="item">The item to display.</param>
         /// <param name="index">The index of the item to display.</param>
         /// <param name="index">The index of the item to display.</param>
         /// <remarks>
         /// <remarks>
-        /// If <see cref="IsItemItsOwnContainer(Control)"/> 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 <see cref="NeedsContainer(object, int, out object?)"/> 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.
         /// </remarks>
         /// </remarks>
         public void PrepareItemContainer(Control container, object? item, int index) => 
         public void PrepareItemContainer(Control container, object? item, int index) => 
             _owner.PrepareItemContainer(container, item, 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
         /// 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.
         /// 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
         /// It must be called regardless of the result of
-        /// <see cref="IsItemItsOwnContainer(Control)"/> but if that method returned true then
-        /// must be called only a single time.
+        /// <see cref="NeedsContainer(object, int, out object?)"/> but if that method returned
+        /// false then must be called only a single time.
         /// </remarks>
         /// </remarks>
         public void ItemContainerPrepared(Control container, object? item, int index) =>
         public void ItemContainerPrepared(Control container, object? item, int index) =>
             _owner.ItemContainerPrepared(container, item, 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
         /// 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
         /// 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
         /// this method is called. This method must not be called if
-        /// <see cref="IsItemItsOwnContainer"/> returned true for the item.
+        /// <see cref="NeedsContainer(object, int, out object?)"/> returned false for the item.
         /// </remarks>
         /// </remarks>
         public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
         public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
 
 

+ 54 - 4
src/Avalonia.Controls/ItemsControl.cs

@@ -306,6 +306,12 @@ namespace Avalonia.Controls
             set => SetValue(AreVerticalSnapPointsRegularProperty, value);
             set => SetValue(AreVerticalSnapPointsRegularProperty, value);
         }
         }
 
 
+        /// <summary>
+        /// Gets a default recycle key that can be used when an <see cref="ItemsControl"/> supports
+        /// a single container type.
+        /// </summary>
+        protected static object DefaultRecycleKey { get; } = new object();
+
         /// <summary>
         /// <summary>
         /// Returns the container for the item at the specified index.
         /// Returns the container for the item at the specified index.
         /// </summary>
         /// </summary>
@@ -361,7 +367,10 @@ namespace Avalonia.Controls
         /// <summary>
         /// <summary>
         /// Creates or a container that can be used to display an item.
         /// Creates or a container that can be used to display an item.
         /// </summary>
         /// </summary>
-        protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
+        protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
+        {
+            return new ContentPresenter();
+        }
 
 
         /// <summary>
         /// <summary>
         /// Prepares the specified element to display the specified item.
         /// Prepares the specified element to display the specified item.
@@ -494,11 +503,52 @@ namespace Avalonia.Controls
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Determines whether the specified item is (or is eligible to be) its own container.
+        /// Determines whether the specified item can be its own container.
         /// </summary>
         /// </summary>
         /// <param name="item">The item to check.</param>
         /// <param name="item">The item to check.</param>
-        /// <returns>true if the item is (or is eligible to be) its own container; otherwise, false.</returns>
-        protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
+        /// <param name="index">The index of the item.</param>
+        /// <param name="recycleKey">
+        /// 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
+        /// <paramref name="recycleKey"/> shoud be set to null.
+        /// </param>
+        /// <returns>
+        /// true if the item needs a container; otherwise false if the item can itself be used
+        /// as a container.
+        /// </returns>
+        protected internal virtual bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
+        {
+            return NeedsContainer<Control>(item, out recycleKey);
+        }
+
+        /// <summary>
+        /// A default implementation of <see cref="NeedsContainerOverride(object, int, out object?)"/>
+        /// that returns true and sets the recycle key to <see cref="DefaultRecycleKey"/> if the item
+        /// is not a <typeparamref name="T"/> .
+        /// </summary>
+        /// <typeparam name="T">The container type.</typeparam>
+        /// <param name="item">The item.</param>
+        /// <param name="recycleKey">
+        /// When the method returns, contains <see cref="DefaultRecycleKey"/> if
+        /// <paramref name="item"/> is not of type <typeparamref name="T"/>; otherwise null.
+        /// </param>
+        /// <returns>
+        /// true if <paramref name="item"/> is of type <typeparamref name="T"/>; otherwise false.
+        /// </returns>
+        protected bool NeedsContainer<T>(object? item, out object? recycleKey) where T : Control
+        {
+            if (item is T)
+            {
+                recycleKey = null;
+                return false;
+            }
+            else
+            {
+                recycleKey = DefaultRecycleKey;
+                return true;
+            }
+        }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

+ 9 - 2
src/Avalonia.Controls/ListBox.cs

@@ -108,8 +108,15 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         public void UnselectAll() => Selection.Clear();
         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<ListBoxItem>(item, out recycleKey);
+        }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnGotFocus(GotFocusEventArgs e)
         protected override void OnGotFocus(GotFocusEventArgs e)

+ 16 - 2
src/Avalonia.Controls/MenuBase.cs

@@ -133,8 +133,22 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         /// <inheritdoc/>
         bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
         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;
+        }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         protected override void OnKeyDown(KeyEventArgs e)

+ 16 - 2
src/Avalonia.Controls/MenuItem.cs

@@ -339,8 +339,22 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         /// <inheritdoc/>
         void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
         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)
         protected override void OnPointerReleased(PointerReleasedEventArgs e)
         {
         {

+ 4 - 4
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@@ -113,14 +113,14 @@ namespace Avalonia.Controls.Presenters
             var generator = itemsControl.ItemContainerGenerator;
             var generator = itemsControl.ItemContainerGenerator;
             Control container;
             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
             else
             {
             {
-                container = generator.CreateContainer();
+                container = (Control)item!;
+                container.SetValue(ItemIsOwnContainerProperty, true);
             }
             }
 
 
             generator.PrepareItemContainer(container, item, index);
             generator.PrepareItemContainer(container, item, index);

+ 9 - 2
src/Avalonia.Controls/Primitives/TabStrip.cs

@@ -16,8 +16,15 @@ namespace Avalonia.Controls.Primitives
             ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
             ItemsPanelProperty.OverrideDefaultValue<TabStrip>(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<TabStripItem>(item, out recycleKey);
+        }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnGotFocus(GotFocusEventArgs e)
         protected override void OnGotFocus(GotFocusEventArgs e)

+ 9 - 2
src/Avalonia.Controls/TabControl.cs

@@ -148,8 +148,15 @@ namespace Avalonia.Controls
             return RegisterContentPresenter(presenter);
             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<TabItem>(item, out recycleKey);
+        }
 
 
         protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
         protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
         {
         {

+ 9 - 2
src/Avalonia.Controls/TreeView.cs

@@ -485,8 +485,15 @@ namespace Avalonia.Controls
             return (false, null);
             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<TreeViewItem>(item, out recycleKey);
+        }
 
 
         protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
         protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
         {
         {

+ 4 - 4
src/Avalonia.Controls/TreeViewItem.cs

@@ -91,14 +91,14 @@ namespace Avalonia.Controls
 
 
         internal TreeView? TreeViewOwner => _treeView;
         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)
         protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)

+ 59 - 38
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@@ -15,13 +15,14 @@ namespace Avalonia.Controls
     /// </summary>
     /// </summary>
     public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
     public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
     {
     {
-        private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
-            AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer");
+        private static readonly AttachedProperty<object?> RecycleKeyProperty =
+            AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
 
 
+        private static readonly object s_itemIsItsOwnContainer = new object();
         private Size _extent;
         private Size _extent;
         private Vector _offset;
         private Vector _offset;
         private Size _viewport;
         private Size _viewport;
-        private Stack<Control>? _recyclePool;
+        private Dictionary<object, Stack<Control>>? _recyclePool;
         private Control? _realized;
         private Control? _realized;
         private int _realizedIndex = -1;
         private int _realizedIndex = -1;
         private Control? _transitionFrom;
         private Control? _transitionFrom;
@@ -172,7 +173,7 @@ namespace Avalonia.Controls
                 return null;
                 return null;
             if (index == _realizedIndex)
             if (index == _realizedIndex)
                 return _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 c;
             return null;
             return null;
         }
         }
@@ -246,10 +247,27 @@ namespace Avalonia.Controls
 
 
         private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
         private Control GetOrCreateElement(IReadOnlyList<object?> 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)
         private Control? GetRealizedElement(int index)
@@ -257,44 +275,37 @@ namespace Avalonia.Controls
             return _realizedIndex == index ? _realized : null;
             return _realizedIndex == index ? _realized : null;
         }
         }
 
 
-        private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
+        private Control GetItemAsOwnContainer(object? item, int index)
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             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<object?> items, int index)
+        private Control? GetRecycledElement(object? item, int index, object? recycleKey)
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             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;
                 recycled.IsVisible = true;
                 generator.PrepareItemContainer(recycled, item, index);
                 generator.PrepareItemContainer(recycled, item, index);
                 generator.ItemContainerPrepared(recycled, item, index);
                 generator.ItemContainerPrepared(recycled, item, index);
@@ -304,14 +315,14 @@ namespace Avalonia.Controls
             return null;
             return null;
         }
         }
 
 
-        private Control CreateElement(IReadOnlyList<object?> items, int index)
+        private Control CreateElement(object? item, int index, object? recycleKey)
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             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);
             generator.PrepareItemContainer(container, item, index);
             AddInternalChild(container);
             AddInternalChild(container);
             generator.ItemContainerPrepared(container, item, index);
             generator.ItemContainerPrepared(container, item, index);
@@ -323,7 +334,10 @@ namespace Avalonia.Controls
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             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;
                 element.IsVisible = false;
             }
             }
@@ -331,7 +345,14 @@ namespace Avalonia.Controls
             {
             {
                 ItemContainerGenerator.ClearItemContainer(element);
                 ItemContainerGenerator.ClearItemContainer(element);
                 _recyclePool ??= new();
                 _recyclePool ??= new();
-                _recyclePool.Push(element);
+
+                if (!_recyclePool.TryGetValue(recycleKey, out var pool))
+                {
+                    pool = new();
+                    _recyclePool.Add(recycleKey, pool);
+                }
+
+                pool.Push(element);
                 element.IsVisible = false;
                 element.IsVisible = false;
             }
             }
         }
         }

+ 69 - 40
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -52,10 +52,11 @@ namespace Avalonia.Controls
                 nameof(VerticalSnapPointsChanged),
                 nameof(VerticalSnapPointsChanged),
                 RoutingStrategies.Bubble);
                 RoutingStrategies.Bubble);
 
 
-        private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
-            AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
+        private static readonly AttachedProperty<object?> RecycleKeyProperty =
+            AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
 
 
         private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
         private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
+        private static readonly object s_itemIsItsOwnContainer = new object();
         private readonly Action<Control, int> _recycleElement;
         private readonly Action<Control, int> _recycleElement;
         private readonly Action<Control> _recycleElementOnItemRemoved;
         private readonly Action<Control> _recycleElementOnItemRemoved;
         private readonly Action<Control, int, int> _updateElementIndex;
         private readonly Action<Control, int, int> _updateElementIndex;
@@ -68,7 +69,7 @@ namespace Avalonia.Controls
         private RealizedStackElements? _realizedElements;
         private RealizedStackElements? _realizedElements;
         private ScrollViewer? _scrollViewer;
         private ScrollViewer? _scrollViewer;
         private Rect _viewport = s_invalidViewport;
         private Rect _viewport = s_invalidViewport;
-        private Stack<Control>? _recyclePool;
+        private Dictionary<object, Stack<Control>>? _recyclePool;
         private Control? _unrealizedFocusedElement;
         private Control? _unrealizedFocusedElement;
         private int _unrealizedFocusedIndex = -1;
         private int _unrealizedFocusedIndex = -1;
 
 
@@ -331,7 +332,7 @@ namespace Avalonia.Controls
                 return null;
                 return null;
             if (_realizedElements?.GetElement(index) is { } realized)
             if (_realizedElements?.GetElement(index) is { } realized)
                 return 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 c;
             return null;
             return null;
         }
         }
@@ -561,10 +562,26 @@ namespace Avalonia.Controls
 
 
         private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
         private Control GetOrCreateElement(IReadOnlyList<object?> 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;
             return e;
         }
         }
 
 
@@ -575,38 +592,33 @@ namespace Avalonia.Controls
             return _realizedElements?.GetElement(index);
             return _realizedElements?.GetElement(index);
         }
         }
 
 
-        private Control? GetItemIsOwnContainer(IReadOnlyList<object?> 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<object?> items, int index)
+        private Control? GetRecycledElement(object? item, int index, object? recycleKey)
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             Debug.Assert(ItemContainerGenerator is not null);
 
 
+            if (recycleKey is null)
+                return null;
+
             var generator = ItemContainerGenerator!;
             var generator = ItemContainerGenerator!;
-            var item = items[index];
 
 
             if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
             if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
             {
             {
@@ -617,9 +629,9 @@ namespace Avalonia.Controls
                 return element;
                 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;
                 recycled.IsVisible = true;
                 generator.PrepareItemContainer(recycled, item, index);
                 generator.PrepareItemContainer(recycled, item, index);
                 generator.ItemContainerPrepared(recycled, item, index);
                 generator.ItemContainerPrepared(recycled, item, index);
@@ -629,14 +641,14 @@ namespace Avalonia.Controls
             return null;
             return null;
         }
         }
 
 
-        private Control CreateElement(IReadOnlyList<object?> items, int index)
+        private Control CreateElement(object? item, int index, object? recycleKey)
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             Debug.Assert(ItemContainerGenerator is not null);
 
 
             var generator = ItemContainerGenerator!;
             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);
             generator.PrepareItemContainer(container, item, index);
             AddInternalChild(container);
             AddInternalChild(container);
             generator.ItemContainerPrepared(container, item, index);
             generator.ItemContainerPrepared(container, item, index);
@@ -650,7 +662,10 @@ namespace Avalonia.Controls
             
             
             _scrollViewer?.UnregisterAnchorCandidate(element);
             _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;
                 element.IsVisible = false;
             }
             }
@@ -663,8 +678,7 @@ namespace Avalonia.Controls
             else
             else
             {
             {
                 ItemContainerGenerator!.ClearItemContainer(element);
                 ItemContainerGenerator!.ClearItemContainer(element);
-                _recyclePool ??= new();
-                _recyclePool.Push(element);
+                PushToRecyclePool(recycleKey, element);
                 element.IsVisible = false;
                 element.IsVisible = false;
             }
             }
         }
         }
@@ -673,19 +687,34 @@ namespace Avalonia.Controls
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             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);
                 RemoveInternalChild(element);
             }
             }
             else
             else
             {
             {
                 ItemContainerGenerator!.ClearItemContainer(element);
                 ItemContainerGenerator!.ClearItemContainer(element);
-                _recyclePool ??= new();
-                _recyclePool.Push(element);
+                PushToRecyclePool(recycleKey, element);
                 element.IsVisible = false;
                 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)
         private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
         {
         {
             Debug.Assert(ItemContainerGenerator is not null);
             Debug.Assert(ItemContainerGenerator is not null);

+ 3 - 3
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -1034,14 +1034,14 @@ namespace Avalonia.Controls.UnitTests
         {
         {
             Type IStyleable.StyleKey => typeof(ItemsControl);
             Type IStyleable.StyleKey => typeof(ItemsControl);
 
 
-            protected internal override Control CreateContainerForItemOverride()
+            protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
             {
             {
                 return new ContainerControl();
                 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<ContainerControl>(item, out recycleKey);
             }
             }
         }
         }
 
 

+ 12 - 4
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@@ -1064,6 +1064,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
 
 
             // Scroll selected item back into view.
             // Scroll selected item back into view.
             scroll.Offset = new(0, 0);
             scroll.Offset = new(0, 0);
+
+            target.PropertyChanged += (s, e) =>
+            {
+                if (e.Property == SelectingItemsControl.SelectedIndexProperty)
+                {
+                }
+            };
+
             Layout(target);
             Layout(target);
 
 
             // The selection should be preserved.
             // The selection should be preserved.
@@ -1387,14 +1395,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
         {
             Type IStyleable.StyleKey => typeof(TestSelector);
             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<TestContainer>(item, out recycleKey);
             }
             }
         }
         }
 
 

+ 4 - 1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -1761,7 +1761,10 @@ namespace Avalonia.Controls.UnitTests
 
 
         private class DerivedTreeViewWithDerivedTreeViewItems : TreeView
         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
         private class DerivedTreeViewItem : TreeViewItem