diff --git a/src/Avalonia.Controls/VirtualizingWrapPanel.cs b/src/Avalonia.Controls/VirtualizingWrapPanel.cs index c2e89c5cac..f379f89025 100644 --- a/src/Avalonia.Controls/VirtualizingWrapPanel.cs +++ b/src/Avalonia.Controls/VirtualizingWrapPanel.cs @@ -54,9 +54,16 @@ namespace Avalonia.Controls private RealizedWrappedElements? _realizedElements; private ScrollViewer? _scrollViewer; private Rect _viewport = s_invalidViewport; - private Stack? _recyclePool; + private Dictionary>? _recyclePool; private Control? _unrealizedFocusedElement; private int _unrealizedFocusedIndex = -1; + private Control? _focusedElement; + private int _focusedIndex = -1; + private static readonly object s_itemIsItsOwnContainer = new object(); + + + private static readonly AttachedProperty RecycleKeyProperty = + AvaloniaProperty.RegisterAttached("RecycleKey"); static VirtualizingWrapPanel() { @@ -297,9 +304,13 @@ namespace Avalonia.Controls { if (index < 0 || index >= Items.Count) return null; - if (_realizedElements?.GetElement(index) is { } realized) + if (_scrollToIndex == index) + return _scrollToElement; + if (_focusedIndex == index) + return _focusedElement; + if (GetRealizedElement(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; } @@ -653,65 +664,81 @@ namespace Avalonia.Controls private Control GetOrCreateElement(IReadOnlyList items, int index) { - var e = GetRealizedElement(index) ?? - GetItemIsOwnContainer(items, index) ?? - GetRecycledElement(items, index) ?? - CreateElement(items, index); - return e; + Debug.Assert(ItemContainerGenerator is not null); + + if ((GetRealizedElement(index) ?? + GetRealizedElement(index, ref _focusedIndex, ref _focusedElement) ?? + GetRealizedElement(index, ref _scrollToIndex, ref _scrollToElement)) is { } realized) + return realized; + + var item = items[index]; + var generator = ItemContainerGenerator!; + + if (generator.NeedsContainer(item, index, out var recycleKey)) + { + return GetRecycledElement(item, index, recycleKey) ?? + CreateElement(item, index, recycleKey); + } + else + { + return GetItemAsOwnContainer(item, index); + } + } + + private Control GetItemAsOwnContainer(object? item, int index) + { + Debug.Assert(ItemContainerGenerator is not null); + + var controlItem = (Control)item!; + var generator = ItemContainerGenerator!; + + if (!controlItem.IsSet(RecycleKeyProperty)) + { + generator.PrepareItemContainer(controlItem, controlItem, index); + AddInternalChild(controlItem); + controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer); + generator.ItemContainerPrepared(controlItem, item, index); + } + + controlItem.IsVisible = true; + return controlItem; } private Control? GetRealizedElement(int index) { - if (_scrollToIndex == index) - return _scrollToElement; return _realizedElements?.GetElement(index); } - private Control? GetItemIsOwnContainer(IReadOnlyList items, int index) + private static Control? GetRealizedElement( + int index, + ref int specialIndex, + ref Control? specialElement) { - var item = items[index]; - - if (item is Control controlItem) + if (specialIndex == index) { - var generator = ItemContainerGenerator!; + Debug.Assert(specialElement is not null); - 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; - } + var result = specialElement; + specialIndex = -1; + specialElement = null; + return result; } return null; } - private Control? GetRecycledElement(IReadOnlyList items, int index) + private Control? GetRecycledElement(object? item, int index, object? recycleKey) { Debug.Assert(ItemContainerGenerator is not null); - var generator = ItemContainerGenerator!; - var item = items[index]; + if (recycleKey is null) + return null; - if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null) - { - var element = _unrealizedFocusedElement; - _unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus; - _unrealizedFocusedElement = null; - _unrealizedFocusedIndex = -1; - return element; - } + 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); @@ -721,14 +748,14 @@ namespace Avalonia.Controls return null; } - private Control CreateElement(IReadOnlyList items, int index) + private Control CreateElement(object? item, int index, object? recycleKey) { Debug.Assert(ItemContainerGenerator is not null); var generator = ItemContainerGenerator!; - var item = items[index]; - var container = generator.CreateContainer(); + var container = generator.CreateContainer(item, index, recycleKey); + container.SetValue(RecycleKeyProperty, recycleKey); generator.PrepareItemContainer(container, item, index); AddInternalChild(container); generator.ItemContainerPrepared(container, item, index); @@ -742,21 +769,26 @@ namespace Avalonia.Controls _scrollViewer?.UnregisterAnchorCandidate(element); - if (element.IsSet(ItemIsOwnContainerProperty)) + var recycleKey = element.GetValue(RecycleKeyProperty); + + if (recycleKey is null) + { + RemoveInternalChild(element); + } + else if (recycleKey == s_itemIsItsOwnContainer) { element.IsVisible = false; } else if (element.IsKeyboardFocusWithin) { - _unrealizedFocusedElement = element; - _unrealizedFocusedIndex = index; - _unrealizedFocusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus; + _focusedElement = element; + _focusedIndex = index; + _focusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus; } else { ItemContainerGenerator!.ClearItemContainer(element); - _recyclePool ??= new(); - _recyclePool.Push(element); + PushToRecyclePool(recycleKey, element); element.IsVisible = false; } } @@ -765,19 +797,33 @@ namespace Avalonia.Controls { Debug.Assert(ItemContainerGenerator is not null); - if (element.IsSet(ItemIsOwnContainerProperty)) + var recycleKey = element.GetValue(RecycleKeyProperty); + + if (recycleKey is null || 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);