TabControl’s ContentTemplate cannot be changed dynamically
I recently used a TabControl and required a different ContentTemplate per item, so I used datatriggers to change the ContentTemplate based on the current selected page. In theory this should work fine, but I found that changes to the ContentTemplate are not effectuated.
How to reproduce
Use the following XAML code to illustrate the problem. The property trigger based on the ‘IsMouseOver’ property changes the content template, when you hover over it. This should make the text to be displayed in bold, but it doesn’t show until you change the page.
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Resources>
<DataTemplate x:Key="TabHeaderTemplate">
<TextBlock Text="{Binding XPath=@header}"/>
</DataTemplate>
<DataTemplate x:Key="TabContent1">
<TextBlock FontWeight="Normal" Text="{Binding XPath=@header}"/>
</DataTemplate>
<DataTemplate x:Key="TabContent2">
<TextBlock FontWeight="Bold" Text="{Binding XPath=@header}"/>
</DataTemplate>
<Style x:Key="DynamicTabControl" TargetType="TabControl">
<Setter Property="ContentTemplate" Value="{StaticResource TabContent1}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="ContentTemplate" Value="{StaticResource TabContent2}"/>
</Trigger>
</Style.Triggers>
</Style>
<XmlDataProvider x:Key="tabPages" XPath="/pages">
<x:XData>
<pages xmlns="">
<page header="Page 1"/>
<page header="Page 2"/>
<page header="Page 3"/>
</pages>
</x:XData>
</XmlDataProvider>
</Page.Resources>
<TabControl ItemsSource="{Binding Source={StaticResource tabPages}, XPath=*}"
ItemTemplate="{StaticResource TabHeaderTemplate}"
Style="{StaticResource DynamicTabControl}"/>
</Page>
What’s the cause?
I was wondering what was happening, so I started my favorite .NET Reflector tool and found out that the ContentTemplate is set in code instead of a binding. The following piece of code takes care of setting the content template and horizontal/vertical alignment (which will probably suffer from the same problem):
private void TabControl.UpdateSelectedContent()
{
// Check if a tab is selected
if (base.SelectedIndex < 0)
{
this.SelectedContent = null;
this.SelectedContentTemplate = null;
this.SelectedContentTemplateSelector = null;
this.SelectedContentStringFormat = null;
}
else
{
// Obtain the selected tab item
TabItem selectedTabItem = this.GetSelectedTabItem();
if (selectedTabItem != null)
{
// Some keyboard stuff removed here...
// Set the content
this.SelectedContent = selectedTabItem.Content;
// Obtain the content presenter for the selected item
ContentPresenter selectedContentPresenter = this.SelectedContentPresenter;
if (selectedContentPresenter != null)
{
// Set horizontal/vertical alignment
selectedContentPresenter.HorizontalAlignment =
selectedTabItem.HorizontalContentAlignment;
selectedContentPresenter.VerticalAlignment = selectedTabItem.VerticalContentAlignment;
}
// Check if the selected tab item defines the content template
if ((selectedTabItem.ContentTemplate != null) ||
(selectedTabItem.ContentTemplateSelector != null) ||
(selectedTabItem.ContentStringFormat != null))
{
// Use the content settings from the tab item
this.SelectedContentTemplate = selectedTabItem.ContentTemplate;
this.SelectedContentTemplateSelector = selectedTabItem.ContentTemplateSelector;
this.SelectedContentStringFormat = selectedTabItem.ContentStringFormat;
}
else
{
// Use the content settings from the tab control
this.SelectedContentTemplate = this.ContentTemplate;
this.SelectedContentTemplateSelector = this.ContentTemplateSelector;
this.SelectedContentStringFormat = this.ContentStringFormat;
}
}
}
}
As you can see, the ContentTemplate is set directly, so updating it after this method has completed doesn’t do anything until the method is invoked again. This method is a private method that is called when one of the following events happen:
- A new template is applied to the TabControl (TabControl.OnApplyTemplate).
- Another tab page is selected (TabControl.OnSelectionChanged).
- abControl’s ItemGenerator status is updated (TabControl.OnGeneratorStatusChanged).
Changes made to the TabControl’s ContentTemplate will therefore only be propagated, when one of these events occur. If you change the ContentTemplate afterwards, then you are out of luck.
How to fix it?
The best way to solve this issue is to use binding, because the binding mechanism will ensure that all properties are updated properly. Unfortunately, you cannot do this, because some of the required properties are read-only and cannot be modified from your code.
The only way to fix it is to override the property metadata for these dependency properties and make sure that the TabControl.UpdateSelectedContent() method is invoked when the dependency property is changed. You cannot call this method directly, but you can call it by calling the public TabControl.OnApplyTemplate() method. The same fix should be made for the TabItem, because if the TabItem has its own template, then this template is used.
I have created a sample application that includes the MyTabControl and MyTabItem classes that contain this workaround.