Browse Source

Support heterogeneous item containers.

pull/11216/head
Steven Kirk 3 years ago
parent
commit
8bfe6b9645
  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();
}
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/>
protected override void OnKeyDown(KeyEventArgs e)

101
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
/// 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
/// container.
/// - The container should then be added to the panel using
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
/// - 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:
///
/// - 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
/// - 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:
///
/// - 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.
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
@ -54,28 +64,43 @@ namespace Avalonia.Controls.Generators
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary>
/// Creates a new container control.
/// Determines whether the specified item needs to be wrapped in a container control.
/// </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>
/// Determines whether the specified item is (or is eligible to be) its own container.
/// Creates a new container control.
/// </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>
/// 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>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
public Control CreateContainer(object? item, int index, object? recycleKey)
=> _owner.CreateContainerForItemOverride(item, index, recycleKey);
/// <summary>
/// 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="index">The index of the item to display.</param>
/// <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>
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
@ -103,8 +128,8 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It must be called regardless of the result of
/// <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>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@ -127,7 +152,7 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container is unrealized. The container must have
/// already have been removed from the virtualizing panel's list of realized containers before
/// this method is called. This method must not be called if
/// <see cref="IsItemItsOwnContainer"/> returned true for the item.
/// <see cref="NeedsContainer(object, int, out object?)"/> returned false for the item.
/// </remarks>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);

58
src/Avalonia.Controls/ItemsControl.cs

@ -306,6 +306,12 @@ namespace Avalonia.Controls
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>
/// Returns the container for the item at the specified index.
/// </summary>
@ -361,7 +367,10 @@ namespace Avalonia.Controls
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ContentPresenter();
}
/// <summary>
/// Prepares the specified element to display the specified item.
@ -494,11 +503,52 @@ namespace Avalonia.Controls
}
/// <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>
/// <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 />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

11
src/Avalonia.Controls/ListBox.cs

@ -108,8 +108,15 @@ namespace Avalonia.Controls
/// </summary>
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/>
protected override void OnGotFocus(GotFocusEventArgs e)

18
src/Avalonia.Controls/MenuBase.cs

@ -133,8 +133,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
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/>
protected override void OnKeyDown(KeyEventArgs e)

18
src/Avalonia.Controls/MenuItem.cs

@ -339,8 +339,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
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)
{

8
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -113,14 +113,14 @@ namespace Avalonia.Controls.Presenters
var generator = itemsControl.ItemContainerGenerator;
Control container;
if (item is Control c && generator.IsItemItsOwnContainer(c))
if (generator.NeedsContainer(item, index, out var recycleKey))
{
container = c;
container.SetValue(ItemIsOwnContainerProperty, true);
container = generator.CreateContainer(item, index, recycleKey);
}
else
{
container = generator.CreateContainer();
container = (Control)item!;
container.SetValue(ItemIsOwnContainerProperty, true);
}
generator.PrepareItemContainer(container, item, index);

11
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -16,8 +16,15 @@ namespace Avalonia.Controls.Primitives
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/>
protected override void OnGotFocus(GotFocusEventArgs e)

11
src/Avalonia.Controls/TabControl.cs

@ -148,8 +148,15 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter);
}
protected internal override Control CreateContainerForItemOverride() => new TabItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabItem>(item, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{

11
src/Avalonia.Controls/TreeView.cs

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

97
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -15,13 +15,14 @@ namespace Avalonia.Controls
/// </summary>
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 Vector _offset;
private Size _viewport;
private Stack<Control>? _recyclePool;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _realized;
private int _realizedIndex = -1;
private Control? _transitionFrom;
@ -172,7 +173,7 @@ namespace Avalonia.Controls
return null;
if (index == _realizedIndex)
return _realized;
if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
@ -246,10 +247,27 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<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)
@ -257,44 +275,37 @@ namespace Avalonia.Controls
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);
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);
var generator = ItemContainerGenerator;
var item = items[index];
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
if (_recyclePool?.Count > 0)
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = _recyclePool.Pop();
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
@ -304,14 +315,14 @@ namespace Avalonia.Controls
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
var container = generator.CreateContainer();
var generator = ItemContainerGenerator!;
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
@ -323,7 +334,10 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
@ -331,7 +345,14 @@ namespace Avalonia.Controls
{
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
element.IsVisible = false;
}
}

109
src/Avalonia.Controls/VirtualizingStackPanel.cs

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

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

@ -1034,14 +1034,14 @@ namespace Avalonia.Controls.UnitTests
{
Type IStyleable.StyleKey => typeof(ItemsControl);
protected internal override Control CreateContainerForItemOverride()
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ContainerControl();
}
protected internal override bool IsItemItsOwnContainerOverride(Control item)
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return item is ContainerControl;
return NeedsContainer<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.Offset = new(0, 0);
target.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
}
};
Layout(target);
// The selection should be preserved.
@ -1387,14 +1395,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Type IStyleable.StyleKey => typeof(TestSelector);
protected internal override bool IsItemItsOwnContainerOverride(Control item)
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return item is TestContainer;
return new TestContainer();
}
protected internal override Control CreateContainerForItemOverride()
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return new TestContainer();
return NeedsContainer<TestContainer>(item, out recycleKey);
}
}

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

@ -1761,7 +1761,10 @@ namespace Avalonia.Controls.UnitTests
private class DerivedTreeViewWithDerivedTreeViewItems : TreeView
{
protected internal override Control CreateContainerForItemOverride() => new DerivedTreeViewItem();
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new DerivedTreeViewItem();
}
}
private class DerivedTreeViewItem : TreeViewItem

Loading…
Cancel
Save