From b57223eb1bd431da1eedddca629221ae476246c5 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Wed, 11 Feb 2026 02:53:07 -0800 Subject: [PATCH] Fix TabItem's Content inheriting TabControl's DataContext instead of TabItem's (#20541) * Child inherit DataContext from TabItem * Remove whitespace changes * Fix nullability error * Fix using Julien's solution --- src/Avalonia.Controls/TabControl.cs | 25 +++++- .../TabControlTests.cs | 77 +++++++++++++++---- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 396f82a622..dc9adc0fac 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Diagnostics; using Avalonia.Collections; using Avalonia.Automation.Peers; using Avalonia.Controls.Presenters; @@ -7,7 +7,6 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; -using Avalonia.VisualTree; using Avalonia.Automation; using Avalonia.Controls.Metadata; using Avalonia.Reactive; @@ -76,7 +75,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - SelectedItemProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent()); + SelectedItemProperty.Changed.AddClassHandler((x, _) => x.UpdateSelectedContent()); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Tab); } @@ -231,7 +230,25 @@ namespace Avalonia.Controls } _selectedItemSubscriptions = new CompositeDisposable( - container.GetObservable(ContentControl.ContentProperty).Subscribe(v => SelectedContent = v), + container.GetObservable(ContentControl.ContentProperty).Subscribe(content => + { + var contentElement = content as StyledElement; + var contentDataContext = contentElement?.DataContext; + SelectedContent = content; + + // When the ContentPresenter (ContentPart) displays content that is a Control, it doesn't + // set its DataContext to that of the Control's. If the content doesn't set a DataContext, + // then it gets inherited from the TabControl. Work around this issue by setting the + // DataContext of the ContentPart to the content's original DataContext (inherited from + // container). + if (contentElement is not null && + contentElement.DataContext != contentDataContext && + ContentPart is not null) + { + Debug.Assert(!contentElement.IsSet(DataContextProperty)); + ContentPart.DataContext = contentDataContext; + } + }), container.GetObservable(ContentControl.ContentTemplateProperty).Subscribe(v => SelectedContentTemplate = SelectContentTemplate(v))); // Note how we fall back to our own ContentTemplate if the container doesn't specify one diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index af0fdd4348..3ea591d361 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -9,6 +9,8 @@ using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.Data; +using Avalonia.Harfbuzz; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; @@ -259,51 +261,48 @@ namespace Avalonia.Controls.UnitTests [Fact] public void DataContexts_Should_Be_Correctly_Set() { + using var app = Start(); var items = new object[] { "Foo", new Item("Bar"), new TextBlock { Text = "Baz" }, new TabItem { Content = "Qux" }, - new TabItem { Content = new TextBlock { Text = "Bob" } } + new TabItem { Content = new TextBlock { Text = "Bob" } }, + new TabItem { DataContext = "Rob", Content = new TextBlock { Text = "Bob" } }, }; var target = new TabControl { - Template = TabControlTemplate(), DataContext = "Base", - DataTemplates = - { - new FuncDataTemplate((x, __) => new Button { Content = x }) - }, ItemsSource = items, }; - ApplyTemplate(target); + var root = CreateRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); - target.ContentPart!.UpdateChild(); - var dataContext = ((TextBlock)target.ContentPart.Child!).DataContext; + var dataContext = ((TextBlock)target.ContentPart!.Child!).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - target.ContentPart.UpdateChild(); dataContext = ((Button)target.ContentPart.Child).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - target.ContentPart.UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - target.ContentPart.UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - target.ContentPart.UpdateChild(); dataContext = target.ContentPart.DataContext; Assert.Equal("Base", dataContext); + + target.SelectedIndex = 5; + dataContext = target.ContentPart.Child.DataContext; + Assert.Equal("Rob", dataContext); } /// @@ -843,6 +842,45 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private static ControlTheme CreateTabControlControlTheme() + { + return new ControlTheme(typeof(TabControl)) + { + Setters = + { + new Setter(TabControl.TemplateProperty, TabControlTemplate()), + }, + }; + } + + private static ControlTheme CreateTabItemControlTheme() + { + return new ControlTheme(typeof(TabItem)) + { + Setters = + { + new Setter(TabItem.TemplateProperty, TabItemTemplate()), + }, + }; + } + + private static TestRoot CreateRoot(Control child) + { + return new TestRoot + { + Resources = + { + { typeof(TabControl), CreateTabControlControlTheme() }, + { typeof(TabItem), CreateTabItemControlTheme() }, + }, + DataTemplates = + { + new FuncDataTemplate((x, _) => new Button { Content = x.Value }) + }, + Child = child, + }; + } + private class TestTopLevel : TopLevel { private readonly ILayoutManager _layoutManager; @@ -892,6 +930,19 @@ namespace Avalonia.Controls.UnitTests target.ContentPart!.ApplyTemplate(); } + private IDisposable Start() + { + return UnitTestApplication.Start( + TestServices.MockThreadingInterface.With( + fontManagerImpl: new HeadlessFontManagerStub(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: () => new KeyboardNavigationHandler(), + inputManager: new InputManager(), + renderInterface: new HeadlessPlatformRenderInterface(), + textShaperImpl: new HarfBuzzTextShaper(), + assetLoader: new StandardAssetLoader())); + } + private class Item { public Item(string value)