Serbin Serbin - 1 month ago 8
C# Question

Style for TabItem's content

I have

TabControl
which is used as a main element of my app:
enter image description here

Every tab content would have a title (duplicate to the menu name), separater and content. My XAML code which must correspond to this view should be like this:

<TabControl Style="{StaticResource MyCustomStyle}">
<TabItem Header="Menu 1">
<TabItem.Content>
...
</TabItem.Content>
</TabItem>
<TabItem Header="Menu 2">
<TabItem.Content>
<TextBlock>Content</TextBlock>
</TabItem.Content>
</TabItem>
...
</TabControl>


To avoid duplicating parts of the code, I decided to setup view in custom style.
MyCustomStyle
for this:

<Style x:Key="MyCustomStyle" TargetType="{x:Type TabControl}">
<Style.Resources>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Padding" Value="10"/>
<Setter Property="Width" Value="120"/>
<Setter Property="Content">
<Setter.Value>
<StackPanel Margin="10">
<Label FontSize="20" Content="..."/>
<Separator/>
<ContentPresenter ContentSource="..."/>
</StackPanel>
</Setter.Value>
</Setter>
</Style>
</Style.Resources>
<Setter Property="TabStripPlacement" Value="Left"/>
</Style>


The only problem occurs with the modification of content. Label don't want to bind value to the
TabItem
's header. The same story for
ContentPresenter


I tried to use
RelativeSource
for this, but this is not working:

<Label FontSize="20" Content="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Header}"/>

Answer

If I understand correctly, you're very close -- you just want to give it a ContentTemplate instead of setting the Content in the style:

<Style x:Key="MyCustomStyle" TargetType="{x:Type TabControl}">
    <Style.Resources>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Padding" Value="10"/>
            <Setter Property="Width" Value="120"/>
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <StackPanel Margin="10">
                            <Label FontSize="20" Content="..."/>
                            <Separator/>
                            <ContentControl 
                                Content="{Binding}"
                                />
                        </StackPanel>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Style.Resources>
    <Setter Property="TabStripPlacement" Value="Left"/>
</Style>

Now, for the Label content. You can't get that via RelativeSource AncestorType because TabItem isn't in the visual tree: If you write a VisualTreeHelper.GetParent() loop, you find that the parent chain hits a bunch of random stuff like Grids and whatnot, then suddenly it's on TabControl.

So what we do instead, is we write a multi-value converter. We give it the DataContext for the DataTemplate -- that's the Content for the TabItem being ContentTemplated -- and the TabControl. Then we look through the TabControl's Items to find the TabItem that has the same Content.

I tried this as a regular valueconverter, just passing in {RelativeSource Self} from the Label, walking the visual tree inside the converter to find the TabControl, and using the Label's DataContext inside the converter to identify the TabItem I wanted. That didn't work because of (I think) virtualization: The DataTemplate is instantiated once and those controls are reused. Since the value of {RelativeSource Self} is the same each time, and I didn't tell the Binding anything about DataContext, the value converter was only invoked for the first TabItem that was ever selected. The multi-value converter fixes that problem by explicitly binding to ., the DataContext.

This breaks if you use ItemsSource to populate the TabControl. In fact, it breaks if you populate the TabControl with anything but TabItems just like you did. But then, you couldn't be setting their Header properties otherwise, and if you were using ItemsSource, you'd have all kinds of good things to bind to and you wouldn't be contemplating nutty expedients like this one.

TabItemHeaderConverter.cs

public class TabItemHeaderConverter : IMultiValueConverter
{
    //  This is pretty awful, but nobody promised life would be perfect. 
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var tc = values[0] as TabControl;
        var tabItemContent = values[1];

        var tabItem = tc.Items.Cast<TabItem>().FirstOrDefault(ti => ti.Content == tabItemContent);

        if (null != tabItem)
        {
            return tabItem.Header;
        }

        return "Unknown";
    }

    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And the new XAML:

<Style x:Key="MyCustomStyle" TargetType="{x:Type TabControl}">
    <Style.Resources>
        <local:TabItemHeaderConverter x:Key="TabItemHeaderConverter" />

        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Padding" Value="10"/>
            <Setter Property="Width" Value="120"/>
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <StackPanel Margin="10">
                            <Label 
                                FontSize="20" 
                                >
                                <Label.Content>
                                    <MultiBinding Converter="{StaticResource TabItemHeaderConverter}">
                                        <Binding RelativeSource="{RelativeSource AncestorType=TabControl}" />
                                        <Binding Path="." />
                                    </MultiBinding>
                                </Label.Content>
                            </Label>
                            <Separator />
                            <ContentControl 
                                Content="{Binding}"
                            />
                        </StackPanel>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Style.Resources>
    <Setter Property="TabStripPlacement" Value="Left"/>
</Style>