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:
<UserControl x:Class="Core2023.SO.Andrew_Williamson.question77744376.CustomTabControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Core2023.SO.Andrew_Williamson.question77744376"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Content="{Binding}">
<UserControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding SelectedContent, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</DataTemplate>
</UserControl.ContentTemplate>
</UserControl>
<UserControl x:Class="Core2023.SO.Andrew_Williamson.question77744376.CustomTabControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Core2023.SO.Andrew_Williamson.question77744376"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Content="{Binding SelectedContent, RelativeSource={RelativeSource Self}}">
</UserControl>
I also suggest you refactor all the code:
using System.Windows;
using System.Windows.Controls;
namespace Core2023.SO.Andrew_Williamson.question77744376
{
public partial class CustomTabItem : CustomTabItemBase
{
public CustomTabItem()
{
InitializeComponent();
}
}
public partial class CustomTabItemBase : UserControl
{
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
RaiseContentChangedEvent(oldContent, newContent);
}
// Register a custom routed event using the Bubble routing strategy.
public static readonly RoutedEvent ContentChangedEvent = EventManager.RegisterRoutedEvent(
name: nameof(ContentChanged),
routingStrategy: RoutingStrategy.Bubble,
handlerType: typeof(ContentChangedEventHandler),
ownerType: typeof(ContentControl));
// Provide CLR accessors for assigning an event handler.
public event ContentChangedEventHandler ContentChanged
{
add => AddHandler(ContentChangedEvent, value);
remove => RemoveHandler(ContentChangedEvent, value);
}
protected void RaiseContentChangedEvent(object? oldContent, object? newContent)
{
// Create a RoutedEventArgs instance.
ContentChangedEventArgs routedEventArgs = new(
routedEvent: ContentChangedEvent,
source: this,
oldContent: oldContent,
newContent: newContent);
// Raise the event, which will bubble up through the element tree.
RaiseEvent(routedEventArgs);
}
}
public delegate void ContentChangedEventHandler(object sender, ContentChangedEventArgs e);
public class ContentChangedEventArgs : RoutedEventArgs
{
public object? OldContent { get; }
public object? NewContent { get; }
public ContentChangedEventArgs(RoutedEvent routedEvent, ContentControl source, object? oldContent, object? newContent)
: base(routedEvent, source)
{
OldContent = oldContent;
NewContent = newContent;
}
}
}
<local:CustomTabItemBase x:Class="Core2023.SO.Andrew_Williamson.question77744376.CustomTabItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Core2023.SO.Andrew_Williamson.question77744376"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<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>
</local:CustomTabItemBase>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
namespace Core2023.SO.Andrew_Williamson.question77744376
{
public partial class CustomTabControl : CustomTabControlBase
{
public CustomTabControl()
{
InitializeComponent();
}
}
[ContentProperty(nameof(Items))]
public class CustomTabControlBase : UserControl
{
public static readonly DependencyPropertyKey ItemsPropertyKey
= DependencyProperty.RegisterReadOnly(
nameof(Items),
typeof(FreezableCollection<CustomTabItemBase>),
typeof(CustomTabControlBase),
new PropertyMetadata(null));
public static readonly DependencyProperty ItemsProperty = ItemsPropertyKey.DependencyProperty;
private static readonly DependencyPropertyKey SelectedContentPropertyKey
= DependencyProperty.RegisterReadOnly(
nameof(SelectedContent),
typeof(object),
typeof(CustomTabControlBase),
new FrameworkPropertyMetadata((object?)null) { });
public static readonly DependencyProperty SelectedContentProperty = SelectedContentPropertyKey.DependencyProperty;
public CustomTabItemBase SelectedTabItem
{
get => (CustomTabItemBase)GetValue(SelectedTabItemProperty);
set => SetValue(SelectedTabItemProperty, value);
}
// Using a DependencyProperty as the backing store for SelectedTabItem. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedTabItemProperty =
DependencyProperty.Register(
nameof(SelectedTabItem),
typeof(CustomTabItemBase),
typeof(CustomTabControlBase),
new PropertyMetadata(null)
{
CoerceValueCallback = (d, value) =>
{
if (((CustomTabControlBase)d).Items.Contains((CustomTabItemBase)value))
{
return value;
}
return null;
},
PropertyChangedCallback = (d, e) =>
{
CustomTabControlBase control = (CustomTabControlBase)d;
if (e.OldValue is CustomTabItemBase old)
{
old.ContentChanged -= control.OnItemContentChanged;
}
CustomTabItemBase @new = (CustomTabItemBase)e.NewValue;
if (@new is not null)
{
@new.ContentChanged += control.OnItemContentChanged;
}
control.SetValue(SelectedContentPropertyKey, @new?.Content);
}
});
private void OnItemContentChanged(object sender, ContentChangedEventArgs e)
{
SetValue(SelectedContentPropertyKey, e.NewContent);
}
public FreezableCollection<CustomTabItemBase> Items
{
get => (FreezableCollection<CustomTabItemBase>)GetValue(ItemsProperty);
private set => SetValue(ItemsPropertyKey, value);
}
public object SelectedContent => GetValue(SelectedContentProperty);
public CustomTabControlBase()
{
Items = new FreezableCollection<CustomTabItemBase>();
Items.Changed += OnItemsChanged;
}
private int itemsCount = 0;
private void OnItemsChanged(object? sender, EventArgs e)
{
int count = Items.Count;
if (itemsCount != count)
{
if (itemsCount == 0) SetValue(SelectedTabItemProperty, Items[0]);
itemsCount = count;
}
InvalidateProperty(SelectedTabItemProperty);
}
}
}
<local:CustomTabControlBase x:Class="Core2023.SO.Andrew_Williamson.question77744376.CustomTabControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Core2023.SO.Andrew_Williamson.question77744376"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Content="{Binding SelectedContent, RelativeSource={RelativeSource Self}}">
</local:CustomTabControlBase>
using System.Windows;
namespace Core2023.SO.Andrew_Williamson.question77744376
{
public partial class CustomTabControlWindow : Window
{
public CustomTabControlWindow()
{
InitializeComponent();
}
}
public class ExampleViewModel
{
public string Example { get; set; } = "Some text";
}
}
<Window x:Class="Core2023.SO.Andrew_Williamson.question77744376.CustomTabControlWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Core2023.SO.Andrew_Williamson.question77744376"
mc:Ignorable="d"
Title="CustomTabControlWindow" Height="450" Width="800">
<Window.DataContext>
<local:ExampleViewModel/>
</Window.DataContext>
<StackPanel>
<!-- The binding works as expected here -->
<TextBlock Text="{Binding Example}" />
<local:CustomTabControl>
<local:CustomTabItem>
<StackPanel>
<TextBlock>Some tab content</TextBlock>
<!-- The data context is always null here for some reason -->
<TextBlock Text="{Binding Example}" />
</StackPanel>
</local:CustomTabItem>
<local:CustomTabItem>
<TextBlock>Some other tab content</TextBlock>
</local:CustomTabItem>
</local:CustomTabControl>
</StackPanel>
</Window>