From 2cc2a91270989d0089da2c7f492dcc3a1b9d2b20 Mon Sep 17 00:00:00 2001 From: timunie Date: Tue, 3 Mar 2026 16:32:48 +0100 Subject: [PATCH 01/11] Add failing test for #14718 --- .../ListBoxVirtualizationIssueTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs index 54fec22e44b..488343e720f 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -258,6 +259,46 @@ public void GhostItemTest_FocusManagement() } } + [Fact] + public void AutoScrollToSelectedItem_Should_Work_When_Becoming_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, + IsVisible = false + }; + + Prepare(target); + + // Select item 50 + target.SelectedIndex = 50; + + // Make visible + target.IsVisible = true; + target.UpdateLayout(); + + // Wait for dispatcher + Dispatcher.UIThread.RunJobs(); + target.UpdateLayout(); + + var scrollViewer = (ScrollViewer)target.VisualChildren[0]; + var offset = scrollViewer.Offset.Y; + + // Item 50 is at 50 * 50 = 2500. + // ListBox height is 100, so it should be visible if offset is between 2400 and 2500. + Assert.True(offset > 0, $"Expected AutoScrollToSelectedItem to scroll to item 50, but offset was {offset}"); + } + } + [Fact] public void GhostItemTest_ScrollToManagement() { From 460e953c2799fad4370ecb0a28ed666c6e550f34 Mon Sep 17 00:00:00 2001 From: timunie Date: Tue, 3 Mar 2026 16:33:10 +0100 Subject: [PATCH 02/11] Fix for AutoScrollToSelectedItemIfNecessary --- .../Primitives/SelectingItemsControl.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index d78d6e07c26..e2b4b85f7f4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -616,6 +616,13 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { AutoScrollToSelectedItemIfNecessary(GetAnchorIndex()); } + else if (change.Property == IsVisibleProperty) + { + if (change.GetNewValue()) + { + AutoScrollToSelectedItemIfNecessary(GetAnchorIndex()); + } + } else if (change.Property == SelectionModeProperty && _selection is object) { var newValue = change.GetNewValue(); @@ -1153,6 +1160,11 @@ Presenter is object && anchorIndex >= 0 && IsAttachedToVisualTree) { + if (!IsEffectivelyVisible) + { + return; + } + Dispatcher.UIThread.Post(state => { ScrollIntoView((int)state!); From 2b7368ca5c985fb8e1afb3bc293068fbd4067750 Mon Sep 17 00:00:00 2001 From: timunie Date: Wed, 4 Mar 2026 08:31:23 +0100 Subject: [PATCH 03/11] fix failing CI build and move test to the right location --- .../ListBoxVirtualizationIssueTests.cs | 41 ----------- .../SelectingItemsControlTests_AutoSelect.cs | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs index 488343e720f..54fec22e44b 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxVirtualizationIssueTests.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; -using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -259,46 +258,6 @@ public void GhostItemTest_FocusManagement() } } - [Fact] - public void AutoScrollToSelectedItem_Should_Work_When_Becoming_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, - IsVisible = false - }; - - Prepare(target); - - // Select item 50 - target.SelectedIndex = 50; - - // Make visible - target.IsVisible = true; - target.UpdateLayout(); - - // Wait for dispatcher - Dispatcher.UIThread.RunJobs(); - target.UpdateLayout(); - - var scrollViewer = (ScrollViewer)target.VisualChildren[0]; - var offset = scrollViewer.Offset.Y; - - // Item 50 is at 50 * 50 = 2500. - // ListBox height is 100, so it should be visible if offset is between 2400 and 2500. - Assert.True(offset > 0, $"Expected AutoScrollToSelectedItem to scroll to item 50, but offset was {offset}"); - } - } - [Fact] public void GhostItemTest_ScrollToManagement() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 0b58a5af437..1e655ae0a47 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -118,6 +120,48 @@ public void Removing_Selected_First_Item_Should_Select_Next_Item() Assert.Equal("bar", target.SelectedItem); } + [Fact] + public void AutoScrollToSelectedItem_Should_Work_When_Becoming_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, + IsVisible = false + }; + + target.Width = target.Height = 100; + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + // Select item 50 + target.SelectedIndex = 50; + + // Make visible + target.IsVisible = true; + target.UpdateLayout(); + + // Wait for dispatcher + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); + target.UpdateLayout(); + + var scrollViewer = (ScrollViewer)target.VisualChildren[0]; + var offset = scrollViewer.Offset.Y; + + // Item 50 is at 50 * 50 = 2500. + // ListBox height is 100, so it should be visible if offset is between 2400 and 2500. + Assert.True(offset > 0, $"Expected AutoScrollToSelectedItem to scroll to item 50, but offset was {offset}"); + } + } + private static FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => @@ -148,5 +192,31 @@ private class ResetOnAdd : List, INotifyCollectionChanged new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } + + private Control CreateListBoxTemplate(TemplatedControl parent, INameScope scope) + { + return new ScrollViewer + { + Name = "PART_ScrollViewer", + Template = new FuncControlTemplate(CreateScrollViewerTemplate), + Content = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + [~ItemsPresenter.ItemsPanelProperty] = + ((ListBox)parent).GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(), + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope); + } + private Control CreateScrollViewerTemplate(TemplatedControl parent, INameScope scope) + { + return new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = + parent.GetObservable(ContentControl.ContentProperty).ToBinding(), + }.RegisterInNameScope(scope); + } + + } } From ea3caf70d091640ed3ce468dca55b05dcec258bf Mon Sep 17 00:00:00 2001 From: timunie Date: Wed, 4 Mar 2026 09:03:09 +0100 Subject: [PATCH 04/11] add failing test for TabItem selection of invisble tab --- .../TabControlTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index fd255f2e7f4..06022157661 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -831,6 +831,33 @@ private static IControlTemplate TabControlTemplate() }); } + [Fact] + public void Only_Fist_Visible_Tab_Should_Be_Selected_By_Default() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + TabItem first, second; + + var target = new TabControl + { + Template = TabControlTemplate(), + Items = + { + (first = new TabItem { Name = "first", Content = "foo", IsVisible = false, }), + (second = new TabItem { Name = "second", Content = "bar", }), + } + }; + + target.ApplyTemplate(); + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(second, target.SelectedItem); + } + } + private static IControlTemplate TabItemTemplate() { return new FuncControlTemplate((parent, scope) => From 84c73d13af3e4111e8c8c3cbdd0f380107c4fc5f Mon Sep 17 00:00:00 2001 From: timunie Date: Wed, 4 Mar 2026 09:43:37 +0100 Subject: [PATCH 05/11] introduce a helper method to figure out which item to select when nothing was selected beforehand and AlwaysSelected is true --- .../Primitives/SelectingItemsControl.cs | 61 +++++++++++++++++-- .../TabControlTests.cs | 17 ++++-- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index e2b4b85f7f4..711b8d026f3 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -461,7 +461,7 @@ private protected override void OnItemsViewCollectionChanged(object? sender, Not if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) { - SelectedIndex = 0; + SelectedIndex = GetFirstVisibleAndEnabledIndex(); } } @@ -534,6 +534,11 @@ protected internal override void ContainerForItemPreparedOverride(Control contai if (Selection.AnchorIndex == index) KeyboardNavigation.SetTabOnceActiveElement(this, container); + + if (AlwaysSelected && index == SelectedIndex && (!container.IsVisible || !container.IsEnabled)) + { + MoveSelectionToFirstVisibleAndEnabledItem(); + } } /// @@ -1042,7 +1047,7 @@ private void OnSelectionModelLostSelection(object? sender, EventArgs e) { if (AlwaysSelected && ItemsView.Count > 0) { - SelectedIndex = 0; + SelectedIndex = GetFirstVisibleAndEnabledIndex(); } } @@ -1217,6 +1222,54 @@ private void MarkContainerSelected(Control container, bool selected) } } + /// + /// Finds the first visible and enabled index in the ItemsSource. + /// + /// the index of the first visible and enabled item, or -1 if none found + private int GetFirstVisibleAndEnabledIndex() + { + var count = ItemCount; + if (count == 0) + return -1; + + for (var i = 0; i < count; i++) + { + var container = ContainerFromIndex(i); + if (container is not null) + { + if (container.IsVisible) + return i; + } + else + { + var item = ItemsView[i]; + if (item is Visual v) + { + if (v.GetValue(IsVisibleProperty) && v.GetValue(IsEnabledProperty)) + return i; + } + else if (item is not null) + { + return i; + } + } + } + + return -1; + } + + /// + /// this method moves selection to first visible and enabled item. + /// + private void MoveSelectionToFirstVisibleAndEnabledItem() + { + var index = GetFirstVisibleAndEnabledIndex(); + if (index != -1 && index != SelectedIndex) + { + SelectedIndex = index; + } + } + private void UpdateContainerSelection() { if (Presenter?.Panel is { } panel) @@ -1263,7 +1316,7 @@ private void InitializeSelectionModel(ISelectionModel model) if (_updateState is null && AlwaysSelected && model.Count == 0) { - model.SelectedIndex = 0; + model.SelectedIndex = GetFirstVisibleAndEnabledIndex(); } UpdateContainerSelection(); @@ -1370,7 +1423,7 @@ private void EndUpdating() if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) { - SelectedIndex = 0; + SelectedIndex = GetFirstVisibleAndEnabledIndex(); } } } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 06022157661..26419c2d184 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -831,20 +831,24 @@ private static IControlTemplate TabControlTemplate() }); } + /// + /// This test ensures that the selected tab is the first visible and enabled tab. + /// [Fact] - public void Only_Fist_Visible_Tab_Should_Be_Selected_By_Default() + public void Only_Fist_Visible_And_Enabled_Tab_Should_Be_Selected_By_Default() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - TabItem first, second; + TabItem expectedSelectionItem; var target = new TabControl { Template = TabControlTemplate(), Items = { - (first = new TabItem { Name = "first", Content = "foo", IsVisible = false, }), - (second = new TabItem { Name = "second", Content = "bar", }), + new TabItem { Name = "first", Content = "foo", IsVisible = false}, + new TabItem { Name = "second", Content = "bar", IsEnabled = false}, + (expectedSelectionItem = new TabItem { Name = "third", Content = "baz" }), } }; @@ -853,8 +857,9 @@ public void Only_Fist_Visible_Tab_Should_Be_Selected_By_Default() var root = new TestRoot(target); root.LayoutManager.ExecuteInitialLayoutPass(); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal(second, target.SelectedItem); + // the 3rd item should be selected + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(expectedSelectionItem, target.SelectedItem); } } From f80df3372124f80f3012cb4d76a6e318f2b8ef37 Mon Sep 17 00:00:00 2001 From: timunie Date: Wed, 4 Mar 2026 10:00:16 +0100 Subject: [PATCH 06/11] ensure selection works for invisible tabcontrol --- .../TabControlTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 26419c2d184..bbb68174c43 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -862,6 +862,42 @@ public void Only_Fist_Visible_And_Enabled_Tab_Should_Be_Selected_By_Default() Assert.Equal(expectedSelectionItem, target.SelectedItem); } } + + /// + /// This test ensures that also an invisible TabControl can receive a selection + /// + [Fact] + public void SelectedIndexShouldRestoreAfterControlGetsVisible() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + TabItem expectedSelectionItem; + + var target = new TabControl + { + IsVisible = false, + SelectedIndex = 1, + Template = TabControlTemplate(), + Items = + { + new TabItem { Name = "first", Content = "foo"}, + (expectedSelectionItem = new TabItem { Name = "second", Content = "bar"}), + new TabItem { Name = "third", Content = "baz" }, + } + }; + + target.ApplyTemplate(); + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.IsVisible = true; + + // the 2nd item should be selected + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(expectedSelectionItem, target.SelectedItem); + } + } private static IControlTemplate TabItemTemplate() { From 5e6103696bd59bc44dafaa0bebd9db4b3a8c440e Mon Sep 17 00:00:00 2001 From: timunie Date: Wed, 4 Mar 2026 10:39:14 +0100 Subject: [PATCH 07/11] simplify conditions --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 711b8d026f3..38aa41dc962 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1237,7 +1237,7 @@ private int GetFirstVisibleAndEnabledIndex() var container = ContainerFromIndex(i); if (container is not null) { - if (container.IsVisible) + if (container is { IsVisible: true, IsEnabled: true }) return i; } else @@ -1245,7 +1245,7 @@ private int GetFirstVisibleAndEnabledIndex() var item = ItemsView[i]; if (item is Visual v) { - if (v.GetValue(IsVisibleProperty) && v.GetValue(IsEnabledProperty)) + if (v.IsVisible && (v is not Control c || c.IsEnabled)) return i; } else if (item is not null) From b28138e4b707368f3d1e2417be4fd2ee0c5f316a Mon Sep 17 00:00:00 2001 From: timunie Date: Wed, 4 Mar 2026 13:19:16 +0100 Subject: [PATCH 08/11] propose: Remove redundant logic from ColorView The TabItem now handles the correct selection of only visible items --- .../ColorView/ColorView.cs | 131 +----------------- 1 file changed, 1 insertion(+), 130 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 100430d74f0..75eea74892d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Media; -using Avalonia.Threading; namespace Avalonia.Controls { @@ -12,7 +11,6 @@ namespace Avalonia.Controls /// Presents a color for user editing using a spectrum, palette and component sliders. /// [TemplatePart("PART_HexTextBox", typeof(TextBox))] - [TemplatePart("PART_TabControl", typeof(TabControl))] public partial class ColorView : TemplatedControl { /// @@ -22,7 +20,6 @@ public partial class ColorView : TemplatedControl // XAML template parts private TextBox? _hexTextBox; - private TabControl? _tabControl; protected bool _ignorePropertyChanged = false; @@ -68,110 +65,7 @@ private void SetColorToHexTextBox() includeSymbol: false); } } - - /// - /// Validates the tab/panel/page selection taking into account the visibility of each item - /// as well as the current selection. - /// - /// - /// Derived controls may re-implement this based on their default style / control template - /// and any specialized selection needs. - /// - protected virtual void ValidateSelection() - { - if (_tabControl != null && - _tabControl.Items != null) - { - // Determine the number of visible tab items - int numVisibleItems = 0; - foreach (var item in _tabControl.Items) - { - if (item is Control control && - control.IsVisible) - { - numVisibleItems++; - } - } - - // Verify the selection - if (numVisibleItems > 0) - { - object? selectedItem = null; - - if (_tabControl.SelectedItem == null && - _tabControl.ItemCount > 0) - { - // As a failsafe, forcefully select the first item - foreach (var item in _tabControl.Items) - { - selectedItem = item; - break; - } - } - else - { - selectedItem = _tabControl.SelectedItem; - } - - if (selectedItem is Control selectedControl && - selectedControl.IsVisible == false) - { - // Select the first visible item instead - foreach (var item in _tabControl.Items) - { - if (item is Control control && - control.IsVisible) - { - selectedItem = item; - break; - } - } - } - - _tabControl.SelectedItem = selectedItem; - _tabControl.IsVisible = true; - } - else - { - // Special case when all items are hidden - // If TabControl ever properly supports no selected item / - // all items hidden this can be removed - _tabControl.SelectedItem = null; - _tabControl.IsVisible = false; - } - - // Hide the "tab strip" if there is only one tab - // This allows, for example, to view only the palette - /* - var itemsPresenter = _tabControl.FindDescendantOfType(); - if (itemsPresenter != null) - { - if (numVisibleItems == 1) - { - itemsPresenter.IsVisible = false; - } - else - { - itemsPresenter.IsVisible = true; - } - } - */ - - // Note that if externally the SelectedIndex is set to 4 or something - // outside the valid range, the TabControl will ignore it and replace it - // with a valid SelectedIndex. This however is not propagated back through - // the TwoWay binding in the control template so the SelectedIndex and - // SelectedIndex become out of sync. - // - // The work-around for this is done here where SelectedIndex is forcefully - // synchronized with whatever the TabControl property value is. This is - // possible since selection validation is already done by this method. - SetCurrentValue(SelectedIndexProperty, _tabControl.SelectedIndex); - } - - return; - } - + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -182,7 +76,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) } _hexTextBox = e.NameScope.Find("PART_HexTextBox"); - _tabControl = e.NameScope.Find("PART_TabControl"); SetColorToHexTextBox(); @@ -193,7 +86,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) } base.OnApplyTemplate(e); - ValidateSelection(); } /// @@ -260,27 +152,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang // (Color will be coerced automatically if HsvColor changes) SetCurrentValue(HsvColorProperty, OnCoerceHsvColor(HsvColor)); } - else if (change.Property == IsColorComponentsVisibleProperty || - change.Property == IsColorPaletteVisibleProperty || - change.Property == IsColorSpectrumVisibleProperty) - { - // When the property changed notification is received here the visibility - // of individual tab items has not yet been updated through the bindings. - // Therefore, the validation is delayed until after bindings update. - Dispatcher.UIThread.Post(() => - { - ValidateSelection(); - }, DispatcherPriority.Background); - } - else if (change.Property == SelectedIndexProperty) - { - // Again, it is necessary to wait for the SelectedIndex value to - // be applied to the TabControl through binding before validation occurs. - Dispatcher.UIThread.Post(() => - { - ValidateSelection(); - }, DispatcherPriority.Background); - } base.OnPropertyChanged(change); } From 608bc93fa0c54faf7c9fc6992a8862a4a0632c55 Mon Sep 17 00:00:00 2001 From: timunie Date: Wed, 4 Mar 2026 14:25:09 +0100 Subject: [PATCH 09/11] fix test: Need to set SelectedIndex after adding items --- tests/Avalonia.Controls.UnitTests/TabControlTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index bbb68174c43..7b6f52734db 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -876,14 +876,14 @@ public void SelectedIndexShouldRestoreAfterControlGetsVisible() var target = new TabControl { IsVisible = false, - SelectedIndex = 1, Template = TabControlTemplate(), Items = { new TabItem { Name = "first", Content = "foo"}, (expectedSelectionItem = new TabItem { Name = "second", Content = "bar"}), new TabItem { Name = "third", Content = "baz" }, - } + }, + SelectedIndex = 1 }; target.ApplyTemplate(); From d214ee4ba4fbf8c806f4f18457083b1cf8fca546 Mon Sep 17 00:00:00 2001 From: timunie Date: Thu, 5 Mar 2026 19:54:17 +0100 Subject: [PATCH 10/11] re-add unused method and make it obsolete Otherwise API-diff will fail. --- .../ColorView/ColorView.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 75eea74892d..bceffee37f1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -65,6 +65,17 @@ private void SetColorToHexTextBox() includeSymbol: false); } } + + /// + /// This method is obsolete now, since the necessary validation is handled by the TabControl. + /// It will be removed in a future release of Avalonia. + /// + // TODO-13: Remove this unused method + [Obsolete] + protected virtual void ValidateSelection() + { + // Empty body for compatibility + } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) From 735712050b2fca08cf5f216ac33bf50499d745b4 Mon Sep 17 00:00:00 2001 From: timunie Date: Sun, 8 Mar 2026 12:24:51 +0100 Subject: [PATCH 11/11] Address review --- .../ColorView/ColorView.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index bceffee37f1..ac0009f7e95 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -67,16 +67,20 @@ private void SetColorToHexTextBox() } /// - /// This method is obsolete now, since the necessary validation is handled by the TabControl. - /// It will be removed in a future release of Avalonia. + /// Validates the tab/panel/page selection taking into account the visibility of each item + /// as well as the current selection. /// + /// + /// Derived controls may re-implement this based on their default style / control template + /// and any specialized selection needs. + /// // TODO-13: Remove this unused method - [Obsolete] + [Obsolete("The necessary validation is now handled by the TabControl. This method will be removed in the next major release.")] protected virtual void ValidateSelection() { - // Empty body for compatibility + // Method is now obsolete and is no longer implemented } - + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) {