I'm refactoring some code at the moment, and am attempting to make a custom 'TabControl'. Ideally I would just style the built-in one but there's some quirks to our codebase and I have to keep the existing behaviour.
I've simplified the example as much as I can. TabControl displays the content of the first TabItem (which is always a UI element) in its content presenter. The content renders as expected, but is not able to access the data context. Can anyone explain why?
My searches so far have found answers that talk about elements not belonging to the logical tree or the visual tree, or to try explicitly setting the DataContext of the content presenter. These answers haven't worked for me.
MainWindow.xaml
<Window x:Class="WpfControls.MainWindow" ...>
<StackPanel>
<!-- The binding works as expected here -->
<TextBlock Text="{Binding Example}" />
<local:TabControl>
<local:TabItem>
<StackPanel>
<TextBlock>Some tab content</TextBlock>
<!-- The data context is always null here for some reason -->
<TextBlock Text="{Binding Example}" />
</StackPanel>
</local:TabItem>
<local:TabItem>
<TextBlock>Some other tab content</TextBlock>
</local:TabItem>
</local:TabControl>
</StackPanel>
</Window>
TabControl.xaml
<UserControl x:Class="WpfControls.Controls.TabControl" ...>
<UserControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding SelectedContent, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</DataTemplate>
</UserControl.ContentTemplate>
</UserControl>
TabControl.xaml.cs
namespace WpfControls.Controls
{
[ContentProperty(nameof(Items))]
public partial class TabControl : UserControl
{
public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(nameof(Items), typeof(List<TabItem>), typeof(TabControl));
private static readonly DependencyPropertyKey SelectedContentPropertyKey = DependencyProperty.RegisterReadOnly(nameof(SelectedContent), typeof(object), typeof(TabControl), new FrameworkPropertyMetadata((object)null));
public static readonly DependencyProperty SelectedContentProperty = SelectedContentPropertyKey.DependencyProperty;
public List<TabItem> Items
{
get => (List<TabItem>)GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
public object SelectedContent => GetValue(SelectedContentProperty);
public TabControl()
{
Items = new List<TabItem>();
Loaded += OnLoaded;
InitializeComponent();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (Items != null && Items.Count > 0)
SetValue(SelectedContentPropertyKey, Items[0].Content);
}
}
}
TabItem.xaml
<UserControl x:Class="WpfControls.Controls.TabItem" ...>
<UserControl.ContentTemplate>
<DataTemplate>
<TextBlock>
There is no ContentPresenter in the tab item.
The content of the selected tab is presented by the parent TabControl.
This template is for the tab header instead.
</TextBlock>
</DataTemplate>
</UserControl.ContentTemplate>
</UserControl>
I agree with @Clemens that you should customize the ItemsControl or TabControl instead of creating a new element from scratch. But I will still show you the errors in your implementation.
You set the ContentTemplate for the CustomTabControl. But this template is used for an object located in the Content property. And this property is empty. Therefore, you need to pass DataContext to this property or set Content instead of ContentTemplate.
ContentPresenter - is not a ContentControl. Therefore, even if Content is set to the content of a UI element, this content will not have the same DataContext as the ContentPresenter itself.
I suggest two fixes:
I also suggest you refactor all the code: