This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
The template is easily one of the most powerful features in Silverlight, and
perhaps one of the most difficult. For that reason, many developers swear by
Expression Blend to generate their templates. This article will demonstrate how
to write templates by hand so you'll be in a better position to understand
Expression Blend output if you later decide to go that route.
ContentControl and DataTemplate
If that object derives from FrameworkElement (such as TextBlock or
Image), then the element is displayed inside the ContentControl. But you can
also set the Content property to an object that does not derive from
FrameworkElement. Here's the Content of a Button set to a
RadialGradientBrush:
<Button
HorizontalAlignment="Center"
VerticalAlignment="Center">
<RadialGradientBrush>
<GradientStop
Offset="0"
Color="Blue" />
<GradientStop
Offset="1"
Color="AliceBlue" />
</RadialGradientBrush>
</Button>
Normally you'd set the Foreground property of a Button to a brush, or the
Background property, or perhaps the BorderBrush property. But setting the
Content property to a brush? What
does that even mean?
If the object set to the
Content property of a
ControlControl
does not derive from FrameworkElement,
it is rendered with its ToString
method, and if the class has no
ToString override, the
fully-qualified class name is displayed, so this particular
Button looks like
this:
This is not exactly something you want to use to show
off your programming skills to your friends.
The existence of the
DataTemplate means that you really can set the
content of a Button to a RadialGradientBrush just as long as you define a visual
tree that makes use of that brush in the DataTemplate:
<Button
HorizontalAlignment="Center"
VerticalAlignment="Center">
<RadialGradientBrush>
<GradientStop
Offset="0"
Color="Blue" />
<GradientStop
Offset="1"
Color="AliceBlue" />
</RadialGradientBrush>
<Button.ContentTemplate>
<DataTemplate>
<Ellipse
Width="100"
Height="100"
Fill="{Binding}"
/>
</DataTemplate>
</Button.ContentTemplate>
</Button>
Notice the Fill property setting of the Ellipse. It's just a Binding markup
extension with no Path settings set. The Fill property doesn't want a particular
property of the RadialGradientBrush. It wants the whole thing. Here's the
Button:
You can use this technique with any ContentControl derivative, or even
ContentControl itself.
Let's define that
DataTemplate in the Resources collection of a
MainPage.xaml files:
<phone:PhoneApplicationPage.Resources>
<DataTemplate
x:Key="brushTemplate">
<Ellipse
Width="100"
Height="100"
Fill="{Binding}"
/>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
Let's give the content panel of this page three Button instances, each
with its ContentTemplate property set to that resource, but with three
different types of Brush objects set to the Content property:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<StackPanel>
<Button
HorizontalAlignment="Center"
ContentTemplate="{StaticResource
brushTemplate}">
<SolidColorBrush
Color="{StaticResource
PhoneAccentColor}" />
</Button>
<Button
HorizontalAlignment="Center"
ContentTemplate="{StaticResource
brushTemplate}">
<RadialGradientBrush>
<GradientStop
Offset="0"
Color="Blue" />
<GradientStop
Offset="1"
Color="AliceBlue" />
</RadialGradientBrush>
</Button>
<Button
HorizontalAlignment="Center"
ContentTemplate="{StaticResource
brushTemplate}">
<LinearGradientBrush>
<GradientStop
Offset="0"
Color="Pink" />
<GradientStop
Offset="1"
Color="Red" />
</LinearGradientBrush>
</Button>
</StackPanel>
</Grid>
Here's the result:
Examining the Visual Tree
I've been mentioning visual trees. Let's look at a
example:
The ButtonTree program lets you dump the visual tree for
a rather conventional Button
(one with its Content
just set to text), a
Button with its
Content property set
to an Image
element, and two others with their Content
properties set to the
RadialGradientBrush
and Clock (as
shown in the examples above) together with a
ContentTemplate. The
program's content Grid
displays each Button
in a cell:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*" />
<ColumnDefinition
Width="*" />
</Grid.ColumnDefinitions>
<Button
Grid.Row="0"
Grid.Column="0"
Content="Click to Dump"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="OnButtonClick"
/>
<Button
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="OnButtonClick">
<Image
Source="ApplicationIcon.png"
Stretch="None" />
</Button>
<Button
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="OnButtonClick">
<Button.Content>
<RadialGradientBrush>
<GradientStop
Offset="0"
Color="Blue" />
<GradientStop
Offset="1"
Color="AliceBlue" />
</RadialGradientBrush>
</Button.Content>
<Button.ContentTemplate>
<DataTemplate>
<Ellipse
Width="100"
Height="100"
Fill="{Binding}"
/>
</DataTemplate>
</Button.ContentTemplate>
</Button>
<Button
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="OnButtonClick">
<Button.Content>
<petzold:Clock
/>
</Button.Content>
<Button.ContentTemplate>
<DataTemplate>
<StackPanel>
<TextBlock
Text="The time is:"
TextAlignment="Center"
/>
<StackPanel
Orientation="Horizontal"
HorizontalAlignment="Center">
<TextBlock
Text="{Binding
Hour}" />
<TextBlock
Text=":" />
<TextBlock
Text="{Binding
Minute}" />
<TextBlock
Text=":" />
<TextBlock
Text="{Binding
Second}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</Button.ContentTemplate>
</Button>
<ScrollViewer
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalScrollBarVisibility="Auto">
<StackPanel
Name="stackPanel" />
</ScrollViewer>
</Grid>
Way down at the bottom is a
StackPanel inside a ScrollViewer for
displaying the visual tree. The code-behind file uses the static
VisualTreeHelper class for enumerating an element's children in a recursive
method, and then displays their names in a hierarchical list:
void
OnButtonClick(object sender,
RoutedEventArgs args)
{
Button btn = sender
as Button;
stackPanel.Children.Clear();
DumpVisualTree(btn, 0);
}
void DumpVisualTree(DependencyObject
parent, int indent)
{
TextBlock txtblk =
new TextBlock();
txtblk.Text = String.Format("{0}{1}",
new string('
', 4 * indent),
parent.GetType().Name);
stackPanel.Children.Add(txtblk);
int numChildren =
VisualTreeHelper.GetChildrenCount(parent);
for (int
childIndex = 0; childIndex < numChildren; childIndex++)
{
DependencyObject child =
VisualTreeHelper.GetChild(parent,
childIndex);
DumpVisualTree(child, indent + 1);
}
}
Click the button in the upper-left corner that has its Content set to text
and the program displays the visual tree of the Button:
ControlTemplate Basics
A DataTemplate allows you to customize the display of content in a
ContentControl. The ControlTemplate-which you can set to the Template property
of any Control-allows you to
customize the appearance of the control itself-what's commonly referred to as
the control "chrome."
Keep in mind that the
ContentTemplate
property is defined by ContentControl
and is only something you'll find only in classes
that derive from ContentControl.
But the Template
property is defined by
Control, and it's
presence is perhaps the primary distinction between controls and
FrameworkElement
derivatives like TextBlock
and Image.
Whenever you think you need a custom control, you should
ask yourself if it is truly a new control you need, or if it's merely a new look
for an existing control. For example, suppose you need a control that has a
particular appearance, and when you tap it, it changes appearance, and then you
tap it again, it goes back to the original appearance. This is a
ToggleButton with just
different visuals-a different
ControlTemplate.
As with styles, very often templates are defined as
resources. Also as with Style,
ControlTemplate
requires a TargetType:
It is very common to see a
Template defined as part of a Style:
<Style
x:Key="btnStyle"
TargetType="Button">
<Setter
Property="Margin"
Value="6" />
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="Button">…</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Notice that property-element syntax is used for the
Setter that
sets the Template
property to an object of type
ControlTemplate.
Defining a template as part of a style is a very common approach because
generally you want to set some properties of the control to make them more
conducive with the template you're building. These
Setter tags
effectively redefine the default property values for the styled and templated
control, but they can still be overridden with local settings on the actual
control.
Let's create a custom
Button. This new
Button will
retain the full functionality of the familiar
Button except that you
(the programmer) will have total control over its appearance. Of course, to keep
it simple, the new Button
won't look all that different from the normal
Button! But it will
show you the concepts involved.
Here's a standard
Button with text content and alignment set so it
takes up only as much space as it needs to display that content:
<Button
Content="Click me!"
HorizontalAlignment="Center"
VerticalAlignment="Center">
</Button>
Now the Button
consists solely of the word "temporary." It doesn't
have any visual feedback when you touch it, but otherwise it's a fully
functional button. It's seriously flawed, of course, because the
Button should really
be displaying "Click me!" but that will be fixed soon.
You can put a
Border around the
TextBlock:
<Button
Content="Click
me!"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Button.Template>
<ControlTemplate
TargetType="Button">
<Border
BorderBrush="{StaticResource
PhoneAccentBrush}"
BorderThickness="6">
<TextBlock
Text="temporary"
/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
Here's what it looks like:
The Visual State Manager
All this time that the Button has been redesigned with a template, it has
otherwise remained a fully-functional button and it's been generating Click
events every time it's been tapped. The big problem is that the Button
does not deliver visual feedback to the
user. It has a customized visual appearance, but that appearance does not
change.
There are really just two features that need to be added
to this template to make it functionally and visually complete:
- The Button needs to provide visual
feedback when the user presses it.
- The Button needs to indicate a disabled
state if it's disabled.
These two features are related because they both involve
changing the visuals of the control under certain circumstances. And the two
features are also related because the solution involves a Silverlight feature
called the Visual State Manager.
The Visual State Manager helps the developer deal with
visual states, which are changes in
control visuals that result from changes in properties (or other states) of the
control. For the Button on Windows Phone 7, the relevant visual states
correspond to the properties IsPressed and IsEnabled.
Let's look at this entire
Style and ControlTemplate in the context of a
page. In the CustomButtonTemplate program, the Style is defined in the page's
Resources collection. Mostly to reduce keep the lines lengths shorter than the
width of the page, the ControlTemplate is defined as a separate resource and
then referenced by the Style. Here's the ControlTemplate first followed by the
Style referencing that template:
<phone:PhoneApplicationPage.Resources>
<ControlTemplate
x:Key="buttonTemplate"
TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup
x:Name="CommonStates">
<VisualState
x:Name="Normal"
/>
<VisualState
x:Name="MouseOver"
/>
<VisualState
x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="border"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame
KeyTime="0:0:0"
Value="{StaticResource
PhoneForegroundBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="contentControl"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame
KeyTime="0:0:0"
Value="{StaticResource
PhoneBackgroundBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState
x:Name="Disabled">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="disableRect"
Storyboard.TargetProperty="Opacity"
To="0.6"
Duration="0:0:0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border
Name="border"
BorderBrush="{TemplateBinding
BorderBrush}"
BorderThickness="{TemplateBinding
BorderThickness}"
Background="{TemplateBinding
Background}"
CornerRadius="12">
<ContentControl
Name="contentControl"
Content="{TemplateBinding
Content}"
ContentTemplate="{TemplateBinding
ContentTemplate}"
Margin="{TemplateBinding
Padding}"
HorizontalAlignment="{TemplateBinding
HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding
VerticalContentAlignment}"
/>
</Border>
<Rectangle
Name="disableRect"
Fill="{StaticResource
PhoneBackgroundBrush}"
Opacity="0"
/>
</Grid>
</ControlTemplate>
<Style
x:Key="buttonStyle"
TargetType="Button">
<Setter
Property="BorderBrush"
Value="{StaticResource
PhoneAccentBrush}"
/>
<Setter
Property="BorderThickness"
Value="6"
/>
<Setter
Property="Background"
Value="{StaticResource
PhoneChromeBrush}"
/>
<Setter
Property="Template"
Value="{StaticResource
buttonTemplate}"
/>
</Style>
</phone:PhoneApplicationPage.Resources>
The content area contains a Button that references this Style, of course, but
I wanted to test the enabling and disabling of the Button in a very interactive
manner, so I added a ToggleButton to the page and set a binding targeting the
IsEnabled property on the styled and templated Button from the IsChecked
property of the ToggleButton.
But it didn't look quite right for the
ToggleButton to be toggled on (that is,
highlighted) when the regular Button was in its normal (that is, enabled) state.
It occurred to me that what I really wanted was for the ToggleButton to actually
say "Button Enabled" when the ToggleButton was toggled on and the Button was
enabled, and for it to say "Button Disabled" when the ToggleButton was toggled
off and the Button was disabled.
This is the beauty of templates. You can do something
like this right in XAML without a whole lot of fuss and without any extra tools
like Expression Blend.
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="*" />
<RowDefinition
Height="*" />
</Grid.RowDefinitions>
<Button
Grid.Row="0"
Content="Click
me!"
Style="{StaticResource
buttonStyle}"
IsEnabled="{Binding
ElementName=toggleButton,
Path=IsChecked}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
<ToggleButton
Name="toggleButton"
Grid.Row="1"
IsChecked="true"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ToggleButton.Template>
<ControlTemplate
TargetType="ToggleButton">
<Border
BorderBrush="{StaticResource
PhoneForegroundBrush}"
BorderThickness="{StaticResource
PhoneBorderThickness}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup
x:Name="CheckStates">
<VisualState
x:Name="Checked">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="txtblk"
Storyboard.TargetProperty="Text">
<DiscreteObjectKeyFrame
KeyTime="0:0:0"
Value="Button Enabled"
/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState
x:Name="Unchecked"
/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock
Name="txtblk"
Text="Button
Disabled"/>
</Border>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Grid>
This
ToggleButton here has what I think of as a single-purpose special-use ad hoc
ControlTemplate, so it doesn't have a lot of extra frills. The visual tree
consists entirely of a Border and a TextBlock. It ignores the Content property,
and the Text property of the TextBlock is initialized with "Button Disabled".
Everything else is done with visual states. In addition to the regular Button
visual states, the ToggleButton also defines a CheckStates group with states
Checked and Unchecked. These are the only states this template handles, and the
animation for the Checked state sets the Text property of the TextBlock to
"Button Enabled." Here it is in action with the Button disabled: