Work with Template, Visual Tree and Visual State Manager in Windows Phone 7


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:

si1.gif

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:

si2.gif

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:

si3.gif

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:

si4.gif

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:

si5.gif

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:

si6.gif


Similar Articles