Nerdy tidbits from my life as a software engineer

Thursday, November 29, 2007

Binding to TabControls

I'm not sure why it took me so long to figure this out, but there is a way to abstractly bind data to TabControls. And it's really not that hard.

There are two template properties on the TabControl object that you can use to set data templates for a TabControl. The first is ItemTemplate. This is the property you use to set the template for the 'Header' of a TabItem. The second property is called 'ContentTemplate'. This is the property that allows you to set the data template for the TabItem.

What's weird is that the ItemTemplate of a TabControl does not control what you would expect the ItemTemplate to control, and there is no HeaderTemplate property. In every other control, if you want to set a template for a header, there is a HeaderTemplate property. If you want to set a template for a list item, you set the ItemTemplate property. The TabControl, however, does not have these properties - or at least, it does, but they're named something else. It's just bizarre that these properties are not named what you would expect them to be named.

Here's some example XAML that does the job:

<TabControl x:Name="_tabControl"
Grid.Row="0"
Grid.Column="2"
BorderThickness="0"
Padding="0"
Margin="0"
ItemTemplate="{StaticResource TabControlHeaderTemplate}"
ContentTemplate="{StaticResource TabItemTemplate}" />

Weird Binding Syntax

One thing that has confused me so far with XAML has been data binding with parenthesis. For example, although I have the following XAML on my XML to XAML using XSLT project, it hasn't made much sense to me until now:

<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="expanderContent"
Storyboard.TargetProperty="(UIElement.Visibility)">

<DiscreteObjectKeyFrame KeyTime="00:00:00"
Value="{x:Static Visibility.Visible}"/>

</ObjectAnimationUsingKeyFrames>

<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="expanderContent"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
>

<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0.001"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.3" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>

There are a few funky things above. The first is that binding syntax that uses parentheses. Why do you have to surround (UIElement.Visibility) with parentheses? Why can't you just use Visibility directly? In other words, why is this invalid:

<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="expanderContent"
Storyboard.TargetProperty="Visibility">
...
</ObjectAnimationUsingKeyFrames>
</Storyboard>

The reason is because Visibility is an attached property! So in order for that property to get assigned to the element we are animating, we have to specify the object for which that attached property is defined. In this case, Visibility is an attached property of the FrameworkElement class. The parentheses tell the interpreter to find the DependencyProperty on FrameworkElement instead of the object itself.

So, armed with this new information, I started to write some of my own attached properties for some extensibility purposes. For instance, I'm writing a new program that has a custom HeaderTemplate for TabControl TabItems. For some of the tab controls, I want to have a close button to the right of the templates text, but for others, I don't want the button. So I wrote a simple class with a single attached property:

/// <summary> 
/// This class declares some attached properties that allow us to extend TabItem! 
/// </summary> 
public class TabItemOptions { 

/// <summary> 
/// This dependency property allows us to 'extend' TabItem without subclassing it! 
/// </summary> 
public static readonly DependencyProperty ShowCloseButtonProperty = 
DependencyProperty.RegisterAttached("ShowCloseButton", 
  typeof(bool), 
  typeof(TabItemOptions), 
  new PropertyMetadata(false)); 

/// <summary> 
/// Returns the show close button property on the given depedency object. 
/// </summary> 
/// <param name="o"></param> 
/// <returns></returns> 
public static bool GetShowCloseButton(DependencyObject o) { 
  return (bool)o.GetValue(ShowCloseButtonProperty); 
} 

/// <summary> 
/// Sets the attached property on the given dependency object. 
/// </summary> 
/// <param name="o"></param> 
/// <param name="showCloseProperty"></param> 
public static void SetShowCloseButton(DependencyObject o, bool showCloseProperty) { 
  o.SetValue(ShowCloseButtonProperty, showCloseProperty); 
} 

}

OK, so that was fairly easy. Now, I had to add some triggers to my data template so that I could automatically show or hide the close button depending on the value of the ShowCloseButton property. This proved more difficult, for some reason:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
x:Class="TabItemStyle"
xmlns:local="clr-namespace:My.Namespace"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Style TargetType="{x:Type TabItem}">

<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>

<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
ToolTip="{TemplateBinding Content}"
ToolTipService.BetweenShowDelay="1"
ToolTipService.HasDropShadow="True"
ToolTipService.ShowDuration="5">

<TextBlock TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"
Margin="0,0,2,0"
FontSize="18"
Text="{TemplateBinding Content}" />

<Button Click="OnCloseTabItemClicked"
Tag="{TemplateBinding Content}"
x:Name="_closeButton">

<Image ToolTip="Close Tab"
Source="Close.ico" />

</Button>

</StackPanel>

<DataTemplate.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<Binding>
<Binding.Path>(local:TabItemOptions.ShowCloseButton)</Binding.Path>
<Binding.RelativeSource>
<RelativeSource Mode="FindAncestor"
AncestorType="{x:Type TabControl}" />
</Binding.RelativeSource>
</Binding>
</DataTrigger.Binding>
<Setter TargetName="_closeButton"
Property="Visibility"
Value="{x:Static Visibility.Visible}" />
</DataTrigger>

<DataTrigger Value="False">
<DataTrigger.Binding>
<Binding>
<Binding.Path>(local:TabItemOptions.ShowCloseButton)</Binding.Path>
<Binding.RelativeSource>
<RelativeSource Mode="FindAncestor"
AncestorType="{x:Type TabControl}" />
</Binding.RelativeSource>
</Binding>
</DataTrigger.Binding>
<Setter TargetName="_closeButton"
Property="Visibility"
Value="{x:Static Visibility.Collapsed}" />
</DataTrigger>

</DataTemplate>

</Setter.Value>
</Setter>
</Style>

</ResourceDictionary>

The thing you may be wondering is why I didn't use the short-hand syntax for the binding. I wish I could answer that question; all that I know is that the following doesn't work:

...
<DataTrigger Value="True" 
 Binding="{Binding (local:TabItemOptions.ShowCloseButton), 
           RelativeSource={
            RelativeSource FindAncestor, 
            AncestorType={x:Type TabControl}}}">
...

In the Visual Studio debugger, I see the following exceptions:

BindingExpression path error: '(local:TabItemOptions.ShowCloseButton)' property not found on 'object' ''TabControl' (Name='_tabControl')'. BindingExpression:Path=(local:TabItemOptions.ShowCloseButton); DataItem='TabControl' (Name='_tabControl'); target element is 'ContentPresenter' (Name=''); target property is 'NoTarget' (type 'Object')

So, for some reason, it has no problem resolving local:TabItemOptions.ShowCloseButton when it is placed inside an element, but not if it is within a markup extension. Strange, right? Maybe I'm missing something...