Browse Source

Merge pull request #11216 from AvaloniaUI/feature/heterogeneous-item-containers-2

Support heterogeneous item containers V2
pull/11283/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
2c3002d9bb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      src/Avalonia.Controls/ComboBox.cs
  2. 101
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  3. 58
      src/Avalonia.Controls/ItemsControl.cs
  4. 11
      src/Avalonia.Controls/ListBox.cs
  5. 18
      src/Avalonia.Controls/MenuBase.cs
  6. 18
      src/Avalonia.Controls/MenuItem.cs
  7. 8
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  8. 11
      src/Avalonia.Controls/Primitives/TabStrip.cs
  9. 11
      src/Avalonia.Controls/TabControl.cs
  10. 11
      src/Avalonia.Controls/TreeView.cs
  11. 8
      src/Avalonia.Controls/TreeViewItem.cs
  12. 97
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  13. 109
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  14. 6
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  15. 16
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  16. 5
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

11
src/Avalonia.Controls/ComboBox.cs

@ -170,8 +170,15 @@ namespace Avalonia.Controls
UpdateFlowDirection(); UpdateFlowDirection();
} }
protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem(); protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem; {
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)

101
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -10,34 +10,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 /// - <see cref="NeedsContainer(object, int, out object?)"/> should first be called to
/// derived from the <see cref="Control"/> class. If this method returns true then the /// determine whether the item needs a container. This method will return true if the item
/// item itself should be used as the container. /// should be wrapped in a container control, or false if the item itself can be used as a
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then /// container.
/// <see cref="CreateContainer"/> should be called to create a new 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 /// NOTE: If <see cref="NeedsContainer(object, int, out object?)"/> in the first step above
/// then the above steps should be carried out a single time; the first time the item is /// returns false then the above steps should be carried out a single time: the first time the
/// displayed. Otherwise the steps should be carried out each time a new container is realized /// item is displayed. Otherwise the steps should be carried out each time a new container is
/// for an item. /// 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 /// - If <see cref="NeedsContainer(object, int, out object?)"/> for the item returned false
/// cannot be unrealized or recycled. /// 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. /// - If recycling is supported by the panel and the container then the container should be
/// - It is assumed that recyclable containers will not be removed from the panel but instead /// added to a recycle pool keyed on the recycle key returned from
/// hidden from view using e.g. `container.IsVisible = false`. /// <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.
@ -55,28 +65,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> /// <param name="item">The item to display.</param>
/// <remarks> /// <param name="index">The index of the item.</param>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be /// <param name="recycleKey">
/// called to determine whether the item itself should be used as a container. After /// When the method returns, contains a key that can be used to locate a previously
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should /// recycled container of the correct type, or null if the item cannot be recycled.
/// be called to prepare the container to display the specified item. /// </param>
/// </remarks> /// <returns>
public Control CreateContainer() => _owner.CreateContainerForItemOverride(); /// 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> /// <param name="item">The item to display.</param>
/// <returns>true if the item is its own container, otherwise false.</returns> /// <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 /// Before calling this method, <see cref="NeedsContainer(object, int, out object?)"/>
/// control items may be; the caller is responsible for checking if each item is a control /// should be called to determine whether the item itself should be used as a container.
/// and calling this method before creating a new 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.
@ -85,10 +110,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 /// If <see cref="NeedsContainer(object, int, out object?)"/> is false for an
/// must only be called a single time, otherwise this method must be called after the /// item, then this method must only be called a single time; otherwise this method must
/// container is created, and each subsequent time the container is recycled to display a /// be called after the container is created, and each subsequent time the container is
/// new item. /// 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);
@ -104,8 +129,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 /// <see cref="NeedsContainer(object, int, out object?)"/> but if that method returned
/// must be called only a single time. /// 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);
@ -128,7 +153,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);

58
src/Avalonia.Controls/ItemsControl.cs

@ -307,6 +307,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>
@ -362,7 +368,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.
@ -495,11 +504,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> /// <param name="index">The index of the item.</param>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true; /// <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)

11
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 Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem; {
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)

18
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 Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator; {
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)

18
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 Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator; {
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)
{ {

8
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 = generator.CreateContainer(item, index, recycleKey);
container.SetValue(ItemIsOwnContainerProperty, true);
} }
else else
{ {
container = generator.CreateContainer(); container = (Control)item!;
container.SetValue(ItemIsOwnContainerProperty, true);
} }
generator.PrepareItemContainer(container, item, index); generator.PrepareItemContainer(container, item, index);

11
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 Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem; {
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)

11
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 Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem; {
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)
{ {

11
src/Avalonia.Controls/TreeView.cs

@ -486,8 +486,15 @@ namespace Avalonia.Controls
return (false, null); return (false, null);
} }
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem(); protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem; {
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)
{ {

8
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)

97
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 = private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer"); 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) ?? Debug.Assert(ItemContainerGenerator is not null);
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ?? var e = GetRealizedElement(index);
CreateElement(items, 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; generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
if (controlItem.IsSet(ItemIsOwnContainerProperty)) controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
{ generator.ItemContainerPrepared(controlItem, item, index);
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;
}
} }
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; if (recycleKey is null)
var item = items[index]; 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 generator = ItemContainerGenerator!;
var item = items[index]; var container = generator.CreateContainer(item, index, recycleKey);
var container = generator.CreateContainer();
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;
} }
} }

109
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 = private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer"); 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) ?? Debug.Assert(ItemContainerGenerator is not null);
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ?? var e = GetRealizedElement(index);
CreateElement(items, 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 controlItem = (Control)item!;
{ var generator = ItemContainerGenerator!;
var generator = ItemContainerGenerator!;
if (controlItem.IsSet(ItemIsOwnContainerProperty)) if (!controlItem.IsSet(RecycleKeyProperty))
{ {
controlItem.IsVisible = true; generator.PrepareItemContainer(controlItem, controlItem, index);
return controlItem; AddInternalChild(controlItem);
} controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
else if (generator.IsItemItsOwnContainer(controlItem)) generator.ItemContainerPrepared(controlItem, item, index);
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
} }
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(item, index, recycleKey);
var container = generator.CreateContainer();
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(); PushToRecyclePool(recycleKey, element);
_recyclePool.Push(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(); PushToRecyclePool(recycleKey, element);
_recyclePool.Push(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);

6
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);
} }
} }

16
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);
} }
} }

5
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

Loading…
Cancel
Save