Search code examples
c#wpfdatacontextcontentpresenter

WPF ContentPresenter content not inheriting DataContext


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>

Solution

  • 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.

    1. 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.

    2. 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>