From 086bd59b1de2b94976ae603e5da7e0e6895b742f Mon Sep 17 00:00:00 2001 From: timunie Date: Fri, 24 Apr 2026 16:21:03 +0200 Subject: [PATCH] address review --- .../Primitives/SelectingItemsControl.cs | 78 +++++++++++-------- .../SelectingItemsControlTests_AutoSelect.cs | 45 +++++++++++ 2 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 756f2c3ca7..93fe096d81 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -478,16 +478,13 @@ namespace Avalonia.Controls.Primitives { base.OnApplyTemplate(e); - void ExecuteScrollWhenLayoutUpdated(object? sender, EventArgs e) - { - LayoutUpdated -= ExecuteScrollWhenLayoutUpdated; - - AutoScrollToSelectedItemIfNecessary(GetAnchorIndex()); - } - if (AutoScrollToSelectedItem) { - LayoutUpdated += ExecuteScrollWhenLayoutUpdated; + Dispatcher.UIThread.Post(static state => + { + var control = (SelectingItemsControl)state!; + control.AutoScrollToSelectedItemIfNecessary(control.GetAnchorIndex()); + }, this); } } @@ -535,7 +532,11 @@ namespace Avalonia.Controls.Primitives if (Selection.AnchorIndex == index) KeyboardNavigation.SetTabOnceActiveElement(this, container); - if (AlwaysSelected && index == SelectedIndex && (!container.IsVisible || !container.IsEnabled)) + if (AlwaysSelected && SelectedIndex == -1 && container is { IsVisible: true, IsEnabled: true }) + { + SelectedIndex = index; + } + else if (AlwaysSelected && index == SelectedIndex && (!container.IsVisible || !container.IsEnabled)) { MoveSelectionToFirstVisibleAndEnabledItem(); } @@ -1158,21 +1159,25 @@ namespace Avalonia.Controls.Primitives } private int? _pendingAutoScrollAnchorIndex; + private void AutoScrollToSelectedItemIfNecessary(int anchorIndex) { if (!(AutoScrollToSelectedItem && !_hasScrolledToSelectedItem && Presenter != null && anchorIndex >= 0 && IsAttachedToVisualTree)) + { + ClearPendingAutoScroll(); return; + } if (!IsEffectivelyVisible) { - // Defer scroll until visible + // Defer scroll until the control becomes effectively visible. _pendingAutoScrollAnchorIndex = anchorIndex; - LayoutUpdated -= OnLayoutUpdatedForAutoScroll; // Avoid duplicate handlers - LayoutUpdated += OnLayoutUpdatedForAutoScroll; + IsEffectivelyVisibleChanged -= OnIsEffectivelyVisibleChangedForAutoScroll; + IsEffectivelyVisibleChanged += OnIsEffectivelyVisibleChangedForAutoScroll; return; } - // If already visible, scroll immediately + ClearPendingAutoScroll(); Dispatcher.UIThread.Post(state => { ScrollIntoView((int)state!); @@ -1180,19 +1185,25 @@ namespace Avalonia.Controls.Primitives }, anchorIndex); } - private void OnLayoutUpdatedForAutoScroll(object? sender, EventArgs e) + private void OnIsEffectivelyVisibleChangedForAutoScroll(object? sender, EventArgs e) { - if (_pendingAutoScrollAnchorIndex != null && IsEffectivelyVisible) + if (!IsEffectivelyVisible || _pendingAutoScrollAnchorIndex is not { } anchorIndex) { - int anchorIndex = _pendingAutoScrollAnchorIndex.Value; - LayoutUpdated -= OnLayoutUpdatedForAutoScroll; - _pendingAutoScrollAnchorIndex = null; - Dispatcher.UIThread.Post(state => - { - ScrollIntoView((int)state!); - _hasScrolledToSelectedItem = true; - }, anchorIndex); + return; } + + ClearPendingAutoScroll(); + Dispatcher.UIThread.Post(state => + { + ScrollIntoView((int)state!); + _hasScrolledToSelectedItem = true; + }, anchorIndex); + } + + private void ClearPendingAutoScroll() + { + _pendingAutoScrollAnchorIndex = null; + IsEffectivelyVisibleChanged -= OnIsEffectivelyVisibleChangedForAutoScroll; } /// @@ -1256,19 +1267,19 @@ namespace Avalonia.Controls.Primitives { if (container is { IsVisible: true, IsEnabled: true }) return i; + + continue; } - else + + var item = ItemsView[i]; + if (item is Visual v) { - var item = ItemsView[i]; - if (item is Visual v) - { - if (v.IsVisible && (v is not Control c || c.IsEnabled)) - return i; - } - else if (item is not null) - { + if (v.IsVisible && (v is not Control c || c.IsEnabled)) return i; - } + } + else if (item is not null) + { + return i; } } @@ -1533,3 +1544,4 @@ namespace Avalonia.Controls.Primitives } } } + diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 4fb3763cad..2d01a2c807 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -162,6 +162,51 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void AutoScrollToSelectedItem_Should_Work_When_Ancestor_Becomes_Visible() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = Enumerable.Range(0, 100).Select(i => $"Item {i}").ToList(); + + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = items, + ItemTemplate = new FuncDataTemplate((_, _) => new TextBlock { Height = 50 }), + Height = 100, + ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel { CacheLength = 0 }), + AutoScrollToSelectedItem = true, + }; + + target.Width = target.Height = 100; + + var host = new StackPanel + { + IsVisible = false, + Children = + { + target, + }, + }; + + var root = new TestRoot(host); + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SelectedIndex = 50; + Assert.False(target.IsEffectivelyVisible); + + host.IsVisible = true; + root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); + root.LayoutManager.ExecuteLayoutPass(); + + var scrollViewer = (ScrollViewer)target.VisualChildren[0]; + var offset = scrollViewer.Offset.Y; + Assert.InRange(offset, 2400, 2500); + } + } + private static FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) =>