TECHNOLOGY USED: SILVERLIGHT 2, BETA 1
In a recent post, I showed how Styles can be used to change one property of the existing appearance of a control, similar to CSS. For example, the Button has a Foreground property that can be used to change the color of the text it contains. You can use a Style object to set this foreground color for several Button controls. Styles have limits. You cannot use a Style to change the shape of a Button. A Style cannot make a rectangular-shaped button look round.
That's where the ControlTemplate is useful. It was designed specifically for changing the shape of a control. It can be used to change that rectangular-shaped button into a round one, as Jesse Liberty demonstrated in his Styles and Templates tutorial. Another excellent post that clearly explains ControlTemplates is Creating a Custom Skin by Matt Berseth.
Some things to notice about Skinning with a ControlTemplate are:
- ControlTemplates are written inside a Style definition, using Property="Template".
- You can mix "normal" Styles and ControlTemplates in the same Style definition. The "normal" Styles become the default Style for your skin.
- The Setter's value must be set to a ControlTemplate.
- The root visual element in the ControlTemplate must be named x:Name="RootElement"
- You can use the property values assigned by the consumer of the template, using the {TemplateBinding PropertyName} syntax.
- Storyboards are used to change the appearance of the control when the visual state changes, for example during the MouseOver event.
- The normal state is the default and starting state of the control. I did not need to provide any animation for this state.
- You can use DoubleAnimation, ColorAnimation, and ObjectAnimationUsingKeyFrames.
- You can animate attached properties, using syntax like (Rectangle.Stroke).(SolidColorBrush.Color).
- You cannot use TemplateBinding values in animations.
- You cannot have two objects in the ControlTemplate with the same x:Name.
- Objects with special names defined in the control contract will receive special functionality from the base control's behavior. For example, a HyperlinkButton's FocusVisualElement will be invisible unless the control has focus... at which time, the FocusVisualElement becomes visible.
An Example Template Skin for HyperlinkButton
The typical example is making a button round. To make my example a little more interesting, I'll do something different: adding a border to a HyperlinkButton, taking care to provide visual clues for all states defined in the HyperlinkButton's control contract: having focus, MouseOver, Pressed, and Disabled. This skin allows for resizing and uses the values assigned by the consumer of this ControlTemplate.
<Application.Resources>
<Style x:Key="HyperlinkWithBorder" TargetType="HyperlinkButton">
<!-- SET DEFAULT STYLE -->
<Setter Property="IsEnabled" Value="true" />
<Setter Property="IsTabStop" Value="true" />
<Setter Property="Foreground" Value="#FF417DA5" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="TextAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="TextWrapping" Value="NoWrap" />
<Setter Property="FontSize" Value="11" />
<!-- Cannot currently parse TextDecorationCollection type in XAML so it's being set in code -->
<!-- <Setter Property="TextDecorations" Value="Underline" /> -->
<!-- Cannot currently parse FontFamily type in XAML so it's being set in code -->
<!-- <Setter Property="FontFamily" Value="Trebuchet MS" /> -->
<!-- Cannot currently parse FontWeight type in XAML so it's being set in code -->
<!-- <Setter Property="FontWeight" Value="Bold" /> -->
<!-- DEFINE CONTROL TEMPLATE -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="HyperlinkButton">
<Grid x:Name="RootElement"
Cursor="{TemplateBinding Cursor}">
<Grid.Resources>
<!-- Visual states of the template -->
<Storyboard x:Key="Normal State" />
<Storyboard x:Key="MouseOver State" >
<DoubleAnimation Duration="0:0:0"
Storyboard.TargetName="FaderRect"
Storyboard.TargetProperty="Opacity"
To=".1" />
<DoubleAnimation Duration="0:0:0"
Storyboard.TargetName="VisualPopRect"
Storyboard.TargetProperty="Opacity"
To="1" />
</Storyboard>
<Storyboard x:Key="Pressed State" >
<DoubleAnimation Duration="0:0:0"
Storyboard.TargetName="FaderRect"
Storyboard.TargetProperty="Opacity"
To=".3" />
<DoubleAnimation Duration="0:0:0"
Storyboard.TargetName="VisualPopRect"
Storyboard.TargetProperty="Opacity"
To="1" />
</Storyboard>
<Storyboard x:Key="Disabled State" >
<DoubleAnimation Duration="0:0:0"
Storyboard.TargetName="FaderRect"
Storyboard.TargetProperty="Opacity"
To=".2" />
<ColorAnimation Duration="0:0:0"
Storyboard.TargetName="FaderRectColor"
Storyboard.TargetProperty="Color"
To="Black" />
<ColorAnimation Duration="0:0:0"
Storyboard.TargetName="BorderRect"
Storyboard.TargetProperty="(Rectangle.Stroke).(SolidColorBrush.Color)"
To="Black" />
</Storyboard>
</Grid.Resources>
<!-- creates the solid line "border" of the HyperlinkButton -->
<Rectangle x:Name="BorderRect"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Stroke="{TemplateBinding Foreground}"
Fill="{TemplateBinding Background}"/>
<!-- Container for positioning HyperlinkButton content -->
<StackPanel Orientation="Horizontal">
<!-- invisible rectangle used to leave space for the highlighter (VisualPopRect) -->
<Rectangle Width="{TemplateBinding Height}"
Height="{TemplateBinding Height}" />
<!-- HyperlinkButton content -->
<ContentPresenter x:Name="Normal"
Background="{TemplateBinding Background}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontStretch="{TemplateBinding FontStretch}"
FontStyle="{TemplateBinding FontStyle}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Padding="{TemplateBinding Padding}"
TextAlignment="{TemplateBinding TextAlignment}"
TextDecorations="{TemplateBinding TextDecorations}"
TextWrapping="{TemplateBinding TextWrapping}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="2,2,2,2" />
</StackPanel>
<!-- Focus indicator, inside dashed line -->
<Rectangle x:Name="FocusVisualElement"
Margin="2" Stretch="Fill"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1" StrokeDashArray="1 2"
/>
<!-- Objects for highlighting changing visual states -->
<!-- Rect to the left of the content -->
<Rectangle x:Name="VisualPopRect"
HorizontalAlignment="Left"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Height}"
Fill="{TemplateBinding Foreground}"
Opacity="0"/>
<!-- Make the control appear faded -->
<Rectangle x:Name="FaderRect"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Opacity="0">
<Rectangle.Fill>
<SolidColorBrush x:Name="FaderRectColor"
Color="White" />
</Rectangle.Fill>
</Rectangle>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
Applying the Template
Using a ControlTemplate is as simple as using a Style... because a ControlTemplate is basically part of a Style. Just use databinding to a StaticResource for linking the Style property to the ControlTemplate. The following code shows three sample HyperlinkButtons: one without the ControlTemplate, one using the default ControlTemplate Values, and one overriding the ControlTemplate's default values.
<Grid x:Name="LayoutRoot" Background="White">
<TextBox Width="100" Height="20" FontSize="10" Margin="0,0,200,150" Text="Click & Tab" />
<HyperlinkButton Content="Default Appearance" Margin="0,0,100,50" Height="20" Width="200"/>
<HyperlinkButton Content="Using ControlTemplate" Margin="0,50,100,0" Height="20" Width="200" Style="{StaticResource HyperlinkWithBorder}" />
<HyperlinkButton x:Name="LinkToDisable" Content="ControlTemplate with inline properties" Margin="0,150,50,0" Height="20" Width="250" Style="{StaticResource HyperlinkWithBorder}" Background="PapayaWhip" Foreground="Sienna"/>
<ToggleButton Content="disable it" Margin="280,150,0,0" Width="60" Height="20" Click="Toggle_Click" x:Name="ButtonThatDisablesLink" ></ToggleButton>
</Grid>
And, finally, here is the Silverlight example you can interact with, to see how it looks when finished.
- To review the MouseOver state, move your mouse around the example.
- To preview the Pressed state, click on the links.
- To preview the Disabled state, click on the button.
- To preview the "has focus" state, click in the textbox and tab to the links.
If you can't see the Silverlight example, below is a screenshot that illustrates the unstyled HyperlinkButton with focus, the ControlTemplate HyperlinkButton with MouseOver, and the second ControlTemplate HyperlinkButton as disabled.
The ControlTemplate is a powerful tool. It can be used to make drastic changes to the way your application looks. For example, in the Solar Shooter game, I made buttons that look like a car, a lightning bolt, and an explosion. ControlTemplates can be more difficult to build than Styles because they include more parts and also because they don't always throw errors - which makes debugging much harder. But the extra work is worth it... and once you learn the concepts, it does get easier. ControlTemplates are one of the Silverlight tools that have the potential to dramatically improve web applications. What will you use them for?