diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index a5079f8344..183882ca50 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -177,6 +177,7 @@ namespace Avalonia.Controls.Presenters
private Control? _child;
private bool _createdChild;
private IRecyclingDataTemplate? _recyclingDataTemplate;
+ private (bool IsSet, object? Value) _overrideDataContext;
private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
///
@@ -420,6 +421,39 @@ namespace Avalonia.Controls.Presenters
///
internal IContentPresenterHost? Host { get; private set; }
+ ///
+ /// Sets the and properties atomically,
+ /// ensuring that the content's DataContext is never temporarily set to an incorrect value.
+ ///
+ /// The new content.
+ /// The DataContext to set on the presenter.
+ ///
+ /// When is set to a , the presenter normally
+ /// clears its to allow the content to inherit it. This method
+ /// overrides that behavior, setting the to the specified value
+ /// before updating the child, preventing any intermediate state where the content could
+ /// inherit an incorrect DataContext from higher up the tree.
+ ///
+ internal void SetContentWithDataContext(object? content, object? dataContext)
+ {
+ _overrideDataContext = (true, dataContext);
+
+ try
+ {
+ SetCurrentValue(ContentProperty, content);
+ }
+ finally
+ {
+ // If Content didn't change, UpdateChild wasn't called and the
+ // override wasn't consumed. Apply the DataContext directly.
+ if (_overrideDataContext.IsSet)
+ {
+ _overrideDataContext = default;
+ DataContext = dataContext;
+ }
+ }
+ }
+
///
public sealed override void ApplyTemplate()
{
@@ -484,8 +518,19 @@ namespace Avalonia.Controls.Presenters
}
}
- // Set the DataContext if the data isn't a control.
- if (contentTemplate is { } || !(content is Control))
+ // Consume the override immediately so any reentrant/cascading calls
+ // to UpdateChild don't incorrectly apply the stale override.
+ var overrideDataContext = _overrideDataContext;
+ _overrideDataContext = default;
+
+ // Set the DataContext: use the caller-provided override if set,
+ // otherwise set to content when a template is present or content
+ // isn't a control, or clear for template-less control content.
+ if (overrideDataContext.IsSet)
+ {
+ DataContext = overrideDataContext.Value;
+ }
+ else if (contentTemplate is { } || !(content is Control))
{
DataContext = content;
}
diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs
index f0c624489f..67274247c2 100644
--- a/src/Avalonia.Controls/TabControl.cs
+++ b/src/Avalonia.Controls/TabControl.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
@@ -297,8 +296,6 @@ namespace Avalonia.Controls
_selectedItemSubscriptions = new CompositeDisposable(
container.GetObservable(ContentControl.ContentProperty).Subscribe(content =>
{
- var contentElement = content as StyledElement;
- var contentDataContext = contentElement?.DataContext;
SelectedContent = content;
if (isInitialFire && shouldTransition)
@@ -306,11 +303,13 @@ namespace Avalonia.Controls
var template = SelectContentTemplate(container.GetValue(ContentControl.ContentTemplateProperty));
SelectedContentTemplate = template;
- _contentPresenter2!.Content = content;
- _contentPresenter2.ContentTemplate = template;
- _contentPresenter2.IsVisible = true;
- if (contentElement is not null && contentElement.DataContext != contentDataContext)
- _contentPresenter2.DataContext = contentDataContext;
+ _contentPresenter2!.ContentTemplate = template;
+ _contentPresenter2!.IsVisible = true;
+
+ if (content is Control && template is null)
+ _contentPresenter2.SetContentWithDataContext(content, container.DataContext);
+ else
+ _contentPresenter2.Content = content;
_pendingForward = forward;
_shouldAnimate = true;
@@ -320,18 +319,11 @@ namespace Avalonia.Controls
{
if (ContentPart != null)
{
- ContentPart.Content = content;
- // When ContentPart displays a Control, it doesn't set its
- // DataContext to that of the Control's. If the content doesn't
- // set a DataContext it gets inherited from the TabControl.
- // Work around this by setting ContentPart's DataContext to
- // the content's original DataContext (inherited from container).
- if (contentElement is not null &&
- contentElement.DataContext != contentDataContext)
- {
- Debug.Assert(!contentElement.IsSet(DataContextProperty));
- ContentPart.DataContext = contentDataContext;
- }
+ var template = SelectContentTemplate(container.GetValue(ContentControl.ContentTemplateProperty));
+ if (content is Control && template is null)
+ ContentPart.SetContentWithDataContext(content, container.DataContext);
+ else
+ ContentPart.Content = content;
}
}
@@ -342,6 +334,24 @@ namespace Avalonia.Controls
SelectedContentTemplate = SelectContentTemplate(v);
if (ContentPart != null && !_shouldAnimate)
ContentPart.ContentTemplate = _selectedContentTemplate;
+ }),
+ container.GetObservable(StyledElement.DataContextProperty).Subscribe(dc =>
+ {
+ // During a transition, ContentPart holds the old tab's content
+ // and _contentPresenter2 holds the new tab's content. Only update
+ // the presenter that is showing this container's content.
+ // Only override DataContext when there's no ContentTemplate;
+ // with a template, the presenter's DataContext should be the
+ // content itself (so the template can bind to it).
+ if (_contentPresenter2 is { IsVisible: true })
+ {
+ if (_contentPresenter2.Content is Control && _contentPresenter2.ContentTemplate is null)
+ _contentPresenter2.DataContext = dc;
+ }
+ else if (ContentPart is { Content: Control } && ContentPart.ContentTemplate is null)
+ {
+ ContentPart.DataContext = dc;
+ }
}));
IDataTemplate? SelectContentTemplate(IDataTemplate? containerTemplate) => containerTemplate ?? ContentTemplate;
diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
index 62c453a18b..8bad019c2b 100644
--- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
@@ -1046,6 +1046,438 @@ namespace Avalonia.Controls.UnitTests
assetLoader: new StandardAssetLoader()));
}
+ [Fact]
+ public void Switching_Tab_Should_Preserve_DataContext_Binding_On_UserControl_Content()
+ {
+ // Issue #18280: When switching tabs, a UserControl inside a TabItem has its
+ // DataContext set to null, causing two-way bindings on child controls (like
+ // DataGrid.SelectedItem) to propagate null back to the view model.
+ // Verify that after switching away and back, the DataContext binding still
+ // resolves correctly.
+ using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
+ var viewModel = new TabDataContextViewModel { SelectedItem = "Item1" };
+
+ // Create a UserControl with an explicit DataContext binding,
+ // matching the issue scenario.
+ var userControl = new UserControl
+ {
+ [~UserControl.DataContextProperty] = new Binding("SelectedItem"),
+ };
+
+ var target = new TabControl
+ {
+ Template = TabControlTemplate(),
+ DataContext = viewModel,
+ Items =
+ {
+ new TabItem
+ {
+ Header = "Tab1",
+ Content = userControl,
+ },
+ new TabItem
+ {
+ Header = "Tab2",
+ Content = "Other content",
+ },
+ },
+ };
+
+ var root = new TestRoot(target);
+ Prepare(target);
+
+ // Verify initial state
+ Assert.Equal(0, target.SelectedIndex);
+ Assert.Equal("Item1", userControl.DataContext);
+
+ // Switch to second tab and back
+ target.SelectedIndex = 1;
+ target.SelectedIndex = 0;
+
+ // The UserControl's DataContext binding should still resolve correctly.
+ Assert.Equal("Item1", userControl.DataContext);
+
+ // Verify the binding is still live by changing the source property.
+ viewModel.SelectedItem = "Item2";
+ Assert.Equal("Item2", userControl.DataContext);
+ }
+
+ [Fact]
+ public void TabItem_Child_DataContext_Binding_Should_Work()
+ {
+ // Issue #20845: When a DataContext binding is placed on the child of a TabItem,
+ // the DataContext is null. The binding hasn't resolved when the content's
+ // DataContext is captured in UpdateSelectedContent, so the captured value is null.
+ using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
+ var viewModel = new MainViewModel();
+
+ var tab1View = new UserControl();
+ tab1View.Bind(UserControl.DataContextProperty, new Binding("Tab1"));
+
+ // Add a child TextBlock that binds to a property on Tab1ViewModel.
+ var textBlock = new TextBlock();
+ textBlock.Bind(TextBlock.TextProperty, new Binding("Name"));
+ tab1View.Content = textBlock;
+
+ var target = new TabControl
+ {
+ Template = TabControlTemplate(),
+ DataContext = viewModel,
+ Items =
+ {
+ new TabItem
+ {
+ Header = "Tab1",
+ Content = tab1View,
+ },
+ },
+ };
+
+ var root = new TestRoot(target);
+ Prepare(target);
+
+ // The UserControl's DataContext should be the Tab1ViewModel.
+ Assert.Same(viewModel.Tab1, tab1View.DataContext);
+
+ // The TextBlock should display the Name from Tab1ViewModel.
+ Assert.Equal("Tab 1 message here", textBlock.Text);
+ }
+
+ [Fact]
+ public void TabItem_Child_With_DataContext_Binding_Should_Propagate_To_Children()
+ {
+ // Issue #20845 (comment): Putting the DataContext binding on the TabItem itself
+ // is also broken. The child should inherit the TabItem's DataContext.
+ using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
+ var viewModel = new MainViewModel();
+
+ var textBlock = new TextBlock();
+ textBlock.Bind(TextBlock.TextProperty, new Binding("Name"));
+ var tab1View = new UserControl { Content = textBlock };
+
+ var target = new TabControl
+ {
+ Template = TabControlTemplate(),
+ DataContext = viewModel,
+ Items =
+ {
+ new TabItem
+ {
+ Header = "Tab1",
+ [~TabItem.DataContextProperty] = new Binding("Tab1"),
+ Content = tab1View,
+ },
+ },
+ };
+
+ var root = new TestRoot(target);
+ Prepare(target);
+
+ // The TabItem's DataContext should be the Tab1ViewModel.
+ var tabItem = (TabItem)target.Items[0]!;
+ Assert.Same(viewModel.Tab1, tabItem.DataContext);
+
+ // The UserControl should inherit the TabItem's DataContext.
+ Assert.Same(viewModel.Tab1, tab1View.DataContext);
+
+ // The TextBlock should display the Name from Tab1ViewModel.
+ Assert.Equal("Tab 1 message here", textBlock.Text);
+ }
+
+ [Fact]
+ public void Switching_Tabs_Should_Not_Null_Out_DataContext_Bound_Properties()
+ {
+ // Issue #20845: DataContext binding should survive tab switches.
+ using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
+ var viewModel = new MainViewModel();
+
+ var tab1View = new UserControl();
+ tab1View.Bind(UserControl.DataContextProperty, new Binding("Tab1"));
+ var textBlock = new TextBlock();
+ textBlock.Bind(TextBlock.TextProperty, new Binding("Name"));
+ tab1View.Content = textBlock;
+
+ var target = new TabControl
+ {
+ Template = TabControlTemplate(),
+ DataContext = viewModel,
+ Items =
+ {
+ new TabItem
+ {
+ Header = "Tab1",
+ Content = tab1View,
+ },
+ new TabItem
+ {
+ Header = "Tab2",
+ Content = "Other content",
+ },
+ },
+ };
+
+ var root = new TestRoot(target);
+ Prepare(target);
+
+ Assert.Same(viewModel.Tab1, tab1View.DataContext);
+ Assert.Equal("Tab 1 message here", textBlock.Text);
+
+ // Switch to tab 2 and back
+ target.SelectedIndex = 1;
+ target.SelectedIndex = 0;
+
+ // DataContext binding should still be resolved correctly.
+ Assert.Same(viewModel.Tab1, tab1View.DataContext);
+ Assert.Equal("Tab 1 message here", textBlock.Text);
+ }
+
+ [Fact]
+ public void Content_Should_Not_Temporarily_Get_Wrong_DataContext_When_Switching_Tabs()
+ {
+ // When ContentPart.Content is set, ContentPresenter.UpdateChild clears its
+ // DataContext before we can set it to the container's DataContext. This causes
+ // the content to briefly inherit TabControl's DataContext instead of TabItem's.
+ using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
+ var viewModel = new MainViewModel();
+
+ var tab1View = new UserControl();
+ var tab2View = new UserControl();
+
+ var target = new TabControl
+ {
+ Template = TabControlTemplate(),
+ DataContext = viewModel,
+ Items =
+ {
+ new TabItem
+ {
+ Header = "Tab1",
+ [~TabItem.DataContextProperty] = new Binding("Tab1"),
+ Content = tab1View,
+ },
+ new TabItem
+ {
+ Header = "Tab2",
+ [~TabItem.DataContextProperty] = new Binding("Tab2"),
+ Content = tab2View,
+ },
+ },
+ };
+
+ var root = new TestRoot(target);
+ Prepare(target);
+
+ Assert.Same(viewModel.Tab1, tab1View.DataContext);
+
+ // Track all DataContext values the new content receives during the switch.
+ var dataContexts = new List