From d0f479d80cf4f805f2357042fd4f3138f7034d42 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 13 Jan 2023 13:29:30 +0100 Subject: [PATCH] Don't recycle focused element. Fixes #9269 --- .../VirtualizingStackPanel.cs | 56 ++++++++++++++++--- .../VirtualizingStackPanelTests.cs | 53 +++++++++++++++++- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 45f1a02a24..3f539ce198 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached("ItemIsOwnContainer"); private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0); - private readonly Action _recycleElement; + private readonly Action _recycleElement; private readonly Action _recycleElementOnItemRemoved; private readonly Action _updateElementIndex; private int _anchorIndex = -1; @@ -38,6 +38,8 @@ namespace Avalonia.Controls private RealizedElementList? _realizedElements; private Rect _viewport = s_invalidViewport; private Stack? _recyclePool; + private Control? _unrealizedFocusedElement; + private int _unrealizedFocusedIndex = -1; public VirtualizingStackPanel() { @@ -167,7 +169,7 @@ namespace Avalonia.Controls _realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex); break; case NotifyCollectionChangedAction.Reset: - _realizedElements.RecycleAllElements(_recycleElementOnItemRemoved); + _realizedElements.ItemsReset(_recycleElementOnItemRemoved); break; } } @@ -482,6 +484,13 @@ namespace Avalonia.Controls var generator = ItemContainerGenerator!; var item = items[index]; + if (_unrealizedFocusedIndex == index) + { + var element = _unrealizedFocusedElement; + _unrealizedFocusedElement = null; + _unrealizedFocusedIndex = -1; + return element; + } if (_recyclePool?.Count > 0) { var recycled = _recyclePool.Pop(); @@ -515,7 +524,7 @@ namespace Avalonia.Controls return index * estimatedElementSize; } - private void RecycleElement(Control element) + private void RecycleElement(Control element, int index) { Debug.Assert(ItemContainerGenerator is not null); @@ -523,6 +532,11 @@ namespace Avalonia.Controls { element.IsVisible = false; } + else if (element.IsKeyboardFocusWithin) + { + _unrealizedFocusedElement = element; + _unrealizedFocusedIndex = index; + } else { ItemContainerGenerator!.ClearItemContainer(element); @@ -932,12 +946,33 @@ namespace Avalonia.Controls } } + /// + /// Recycles all elements in response to the source collection being reset. + /// + /// A method used to recycle elements. + public void ItemsReset(Action recycleElement) + { + if (_elements is null || _elements.Count == 0) + return; + + foreach (var e in _elements) + { + if (e is not null) + recycleElement(e); + } + + _startU = _firstIndex = 0; + _elements?.Clear(); + _sizes?.Clear(); + + } + /// /// Recycles elements before a specific index. /// /// The index in the source collection of new first element. /// A method used to recycle elements. - public void RecycleElementsBefore(int index, Action recycleElement) + public void RecycleElementsBefore(int index, Action recycleElement) { if (index <= FirstIndex || _elements is null || _elements.Count == 0) return; @@ -953,7 +988,7 @@ namespace Avalonia.Controls for (var i = 0; i < endIndex; ++i) { if (_elements[i] is Control e) - recycleElement(e); + recycleElement(e, i + FirstIndex); } _elements.RemoveRange(0, endIndex); @@ -967,7 +1002,7 @@ namespace Avalonia.Controls /// /// The index in the source collection of new last element. /// A method used to recycle elements. - public void RecycleElementsAfter(int index, Action recycleElement) + public void RecycleElementsAfter(int index, Action recycleElement) { if (index >= LastIndex || _elements is null || _elements.Count == 0) return; @@ -984,7 +1019,7 @@ namespace Avalonia.Controls for (var i = startIndex; i < count; ++i) { if (_elements[i] is Control e) - recycleElement(e); + recycleElement(e, i + FirstIndex); } _elements.RemoveRange(startIndex, _elements.Count - startIndex); @@ -996,15 +1031,18 @@ namespace Avalonia.Controls /// Recycles all realized elements. /// /// A method used to recycle elements. - public void RecycleAllElements(Action recycleElement) + public void RecycleAllElements(Action recycleElement) { if (_elements is null || _elements.Count == 0) return; + var i = FirstIndex; + foreach (var e in _elements) { if (e is not null) - recycleElement(e); + recycleElement(e, i); + ++i; } _startU = _firstIndex = 0; diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index aa521c0d37..f1dd874c71 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -227,6 +227,57 @@ namespace Avalonia.Controls.UnitTests Assert.Null(removed.VisualParent); } + [Fact] + public void Does_Not_Recycle_Focused_Element() + { + using var app = App(); + var (target, scroll, itemsControl) = CreateTarget(); + + target.GetRealizedElements().First()!.Focus(); + Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin); + + scroll.Offset = new Vector(0, 200); + Layout(target); + + Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin)); + } + + [Fact] + public void Removing_Item_Of_Focused_Element_Clears_Focus() + { + using var app = App(); + var (target, scroll, itemsControl) = CreateTarget(); + + var focused = target.GetRealizedElements().First()!; + focused.Focus(); + Assert.True(focused.IsKeyboardFocusWithin); + + scroll.Offset = new Vector(0, 200); + Layout(target); + + Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin)); + Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x)); + } + + [Fact] + public void Scrolling_Back_To_Focused_Element_Uses_Correct_Element() + { + using var app = App(); + var (target, scroll, itemsControl) = CreateTarget(); + + var focused = target.GetRealizedElements().First()!; + focused.Focus(); + Assert.True(focused.IsKeyboardFocusWithin); + + scroll.Offset = new Vector(0, 200); + Layout(target); + + scroll.Offset = new Vector(0, 0); + Layout(target); + + Assert.Same(focused, target.GetRealizedElements().First()); + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() @@ -326,6 +377,6 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(ns)); } - private static IDisposable App() => UnitTestApplication.Start(); + private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus); } }