Sharing and Reusing Styles and Templates 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

It's possible to derive one Style from another, in the process inheriting all the Setter objects. The new Style can add to those Setter objects or override them. However, it is not possible to derive from a ControlTemplate. There's no way to reference an existing ControlTemplate and specify an additional piece of the visual tree, or a replacement for part of the visual tree. (It's hard enough imaging the mechanics or syntax of such a process.)

Here's a program called FlipToggleDemo that includes a custom class named FlipToggleButton that derives from ToggleButton. But FlipToggleButton doesn't add any code to ToggleButton- just a Style and ControlTemplate.

In the FlipToggleDemo project, I added a new item of type Windows Phone User Control and I gave it a name of FlipToggleButton.xaml. This process creates a FlipToggleButton.xaml file and a FlipToggleButton.xaml.cs file for a class that derives from UserControl. But then I went into both files and changed UserControl to ToggleButton so FlipToggleButton derives from ToggleButton.

To keep things simple, I decided not to implement any state transitions for a disabled button, but to flip the button upside down for the Unchecked state. Here's the complete XAML file for the custom button, with indentation reduced to 2 spaces to avoid lines wider than the pages of this book:

<ToggleButton x:Class="FlipToggleDemo.FlipToggleButton"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ToggleButton.Style>
        <Style TargetType="ToggleButton">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ToggleButton">
                        <Border BorderBrush="{StaticResource PhoneForegroundBrush}"
                                BorderThickness="{StaticResource PhoneBorderThickness}"
                                Background="{TemplateBinding Background}"
                                RenderTransformOrigin="0.5 0.5">
                            <Border.RenderTransform>
                                <RotateTransform x:Name="rotate" />
                            </Border.RenderTransform>
                                <VisualStateManager.VisualStateGroups>
                                    <VisualStateGroup x:Name="CheckStates">
                                        <VisualState x:Name="Checked">
                                            <Storyboard>
                                                <DoubleAnimation Storyboard.TargetName="rotate"
                                                                 Storyboard.TargetProperty="Angle"
                                                                 To="180" Duration="0:0:0.5" />
                                            </Storyboard>
                                        </VisualState> 
                                        <VisualState x:Name="Unchecked">
                                            <Storyboard>
                                                <DoubleAnimation Storyboard.TargetName="rotate"
                                                                 Storyboard.TargetProperty="Angle"
                                                                 Duration="0:0:0.5" />
                                            </Storyboard>
                                        </VisualState>
                                    </VisualStateGroup>
                                </VisualStateManager.VisualStateGroups>                           
                                
<ContentPresenter Content="{TemplateBinding Content}"
                                                  ContentTemplate="{TemplateBinding ContentTemplate}"
                                                  Margin="{TemplateBinding Padding}"
                                                  HorizontalAlignment="{TemplateBinding
                                                               HorizontalContentAlignment}"
                                                  VerticalAlignment="{TemplateBinding
                                                               VerticalContentAlignment}" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ToggleButton.Style
</ToggleButton>
 

Usually when you're looking at XAML file, the bulk of the file is set to the Content property of the root element. Here the bulk of the file is set to the Style property of the root element. Notice that the Style object and the ControlTemplate object both have TargetType set to ToggleButton rather than FlipToggleButton. This is fine because neither references any properties specifically defined by FlipToggleButton because FlipToggleButton does not define any new properties.

The two animations that flip the button upside down (for the Checked state) and back (for the Unchecked state) have non-zero times. The DoubleAnimation for the Checked state has no From value; it uses the base value of the property, which is zero. The DoubleAnimation for the Unchecked state has neither a To or From value! The animation starts at whatever value the Angle property of the RotateTransform happens to be-probably 180 but perhaps something lower if the animation to flip the button hasn't quite completed when the button is unchecked-and it ends at the base value, which is zero.

Here's the complete code-behind file for the custom control:

using System.Windows.Controls.Primitives;

namespace
FlipToggleDemo
{
    public partial class FlipToggleButton : ToggleButton
    {
        public FlipToggleButton()
        {
            InitializeComponent();
        }
    }
}

The MainPage.xaml file of the project instantiates the custom button to test it out:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <local:FlipToggleButton Content="Flip Toggle"
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center" />
        </Grid
>

Custom Controls in a Library

Generally when you create a custom control, you define some new properties for the control as well as a default Style and ControlTemplate, and you put that new control in a DLL for sharing among multiple applications. You can couple the code and Style as shown in the FlipToggleButton example, but a more standard approach for Silverlight libraries involves defining the Style in a special file named generic.xaml located in a directory named Themes. This generic.xaml file has a root element of ResourceDictionary.

Suppose you conceive of a ToggleButton template something like the one in the CustomButtonTemplate project but much more generalized. Rather than just switch between two hard-coded text strings, you want to switch between two objects of any type. And not just switch-you want an object associated with the Checked state and an object associated with the Unchecked state to fade from one to the other. Your name for this new button is called FadableToggleButton.

I defined this class in the Petzold.Phone.Silverlight library. The complete code for FadableToggleButton is here:

using System.Windows;
using System.Windows.Controls.Primitives;
namespace
Petzold.Phone.Silverlight
{
    public class FadableToggleButton : ToggleButton
    {
        public static readonly DependencyProperty CheckedContentProperty =
            DependencyProperty.Register("CheckedContent", typeof(object),
            typeof(FadableToggleButton), new PropertyMetadata(null));
        public FadableToggleButton()
        {
            this.DefaultStyleKey = typeof(FadableToggleButton);
        }
        public object CheckedContent
        {
            set { SetValue(CheckedContentProperty, value); }
            get { return (object)GetValue(CheckedContentProperty); }
        }
    }
}

This is the only C# code required to implement this control! There's not even a property-changed handler for this new CheckedContent property. It's just a DependencyProperty definition and a CLR property definition. Everything else is XAML.

Silverlight looks in a very special XAML file in the library. This XAML file is always named generic.xaml and it is always located in a directory named Themes of the DLL project. This is how a control gets a default theme style and template.

This generic.xaml file has a root element of ResourceDictionary. However the file is special in another way: The contents are regarded as resources but the Style elements don't require x:Key or x:Name attributes because they are referenced via the TargetType.

Here's the portion of the generic.xaml file in the Themes directory of Petzold.Phone.Silverlight that contains the default Style definition of the FadableToggleButton class:

<ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Petzold.Phone.Silverlight">
   
   
<Style TargetType="local:FadableToggleButton">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:FadableToggleButton">
                    <Grid>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="MouseOver" />
                                <VisualState x:Name="Pressed" />
                                <VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="disableRect"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0.6" Duration="0:0:0" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="CheckStates">
                                <VisualState x:Name="Checked">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="uncheckedContent"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0" Duration="0:0:0.5" />
                                        <DoubleAnimation Storyboard.TargetName="checkedContent"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="1" Duration="0:0:0.5" />
                                    </Storyboard>
                                </VisualState> 
                                <VisualState x:Name="Unchecked">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="uncheckedContent"
                                                         Storyboard.TargetProperty="Opacity"
                                                         Duration="0:0:0.5" />
                                        <DoubleAnimation Storyboard.TargetName="checkedContent"
                                                         Storyboard.TargetProperty="Opacity"
                                                         Duration="0:0:0.5" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Border BorderBrush="{StaticResource PhoneForegroundBrush}"
                                BorderThickness="{StaticResource PhoneBorderThickness}"
                                Background="{TemplateBinding Background}"> 
                            <Grid Margin="{TemplateBinding Padding}">
                                <ContentPresenter Name="uncheckedContent"
                                                  Content="{TemplateBinding Content}"
                                                  ContentTemplate="{TemplateBinding ContentTemplate}"
                                                  HorizontalAlignment="{TemplateBinding
                                                                 HorizontalContentAlignment}"
                                                  VerticalAlignment="{TemplateBinding
                                                                 VerticalContentAlignment}" /> 
                                <ContentPresenter Name="checkedContent"
                                                  Opacity="0"
                                                  Content="{TemplateBinding CheckedContent}"
                                                  ContentTemplate="{TemplateBinding ContentTemplate}"
                                                  HorizontalAlignment="{TemplateBinding
                                                                HorizontalContentAlignment}"
                                                  VerticalAlignment="{TemplateBinding
                                                                VerticalContentAlignment}" />
                            </Grid>
                        </Border> 
                        <Rectangle Name="disableRect"
                                   Fill="{StaticResource PhoneBackgroundBrush}"
                                   Opacity="0" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</
ResourceDictionary
>

The TargetType for the Style is FadableToggleButton, and that's enough to allow Silverlight to find this Style definition that becomes the default theme for the FadableToggleButton. Within the Border is a single-cell Grid with two ContentPresenter elements, one with a TemplateBinding referencing the normal Content property, the other referencing the CheckedContent property. The ContentPresenter referencing the CheckedContent property has an initial Opacity of zero. The animations for the Checked and Unchecked states target the Opacity property of the ContentPresenter so that one fades out as the other fades in.

To test out this new control, I created a FadableToggleDemo program. The project contains a reference to the Petzold.Phone.Silverlight library and an XML namespace declaration for the library in MainPage.xaml. I added two bitmaps of the same size to an Images directory in the project. These bitmaps are referenced by Image elements set to the Content and CheckedContent properties of the button:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">            
           
<petzold:FadableToggleButton HorizontalAlignment="Center"
                                         VerticalAlignment="Center">
                <petzold:FadableToggleButton.Content>
                    <Image Source="Images/MunchScream.jpg"
                           Stretch="None" />
                </petzold:FadableToggleButton.Content>
                <petzold:FadableToggleButton.CheckedContent>
                    <Image Source="Images/BotticelliVenus.jpg"
                           Stretch="None" />
                </petzold:FadableToggleButton.CheckedContent>               
           
</petzold:FadableToggleButton>
        </Grid
>

The Content property is set to an image from Edvard Munch's painting The Scream:

siss1.gif

The CheckedContent property uses Botticelli's Birth of Venus:

siss2.gif

Variations on the Slider

As you might expect, the Slider has one of the more complex templates in all of standard Silverlight, and for that reason, it's important to get familiar with it-particularly if you're not a big fan of the default Slider template implemented in Windows Phone 7.

At first, a Slider does not seem to fit into the scheme of templates, primarily because it contains moving parts. How does this work exactly?

When designing a new template for the Slider, the most straightfoward approach is to use a single-cell Grid to enclose the two templates. A nested Grid named "HorizontalTemplate" contains three columns with the two RepeatButton controls and a Thumb. Another nested Grid named "VerticalTemplate" has three rows.

Here's is what I think of as a "bare bones" template for Slider defined as a resource:

    <
phone:PhoneApplicationPage.Resources>
        <ControlTemplate x:Key="bareBonesSliderTemplate"
                         TargetType="Slider">
            <Grid>
                <Grid Name="HorizontalTemplate">
                    <Grid.ColumnDefinitions>
                       
<ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <RepeatButton Name="HorizontalTrackLargeChangeDecreaseRepeatButton"
                                  Grid.Column="0"
                                  Content="-" />
                    <Thumb Name="HorizontalThumb"
                           Grid.Column="1" /> 
                    <RepeatButton Name="HorizontalTrackLargeChangeIncreaseRepeatButton"
                                  Grid.Column="2"
                                  Content="+" />
                </Grid>
                <Grid Name="VerticalTemplate">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions> 
                    <RepeatButton Name="VerticalTrackLargeChangeDecreaseRepeatButton"
                                  Grid.Row="0"
                                  Content="-" /> 
                    <Thumb Name="VerticalThumb"
                           Grid.Row="1" /> 
                    <RepeatButton Name="VerticalTrackLargeChangeIncreaseRepeatButton"
                                  Grid.Row="2"
                                  Content="+" />
                </Grid>
            </Grid>
        </ControlTemplate>
    </phone:PhoneApplicationPage.Resources>

The Slider changes the Value property (and consequently the relative size of the two RepeatButton controls) when the user presses a RepeatButton or physically moves the Thumb. (I'll discuss the Thumb control in more detail soon.)

The BareBonesSlider project continues by instantiating two Slider controls in its content area and applying the template:

        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Slider Grid.Row="0"
                    Orientation="Horizontal"
                    Template="{StaticResource bareBonesSliderTemplate}" />
            <Slider Grid.Row="1"
                    Orientation="Vertical"
                    Template="{StaticResource bareBonesSliderTemplate}"
                    HorizontalAlignment="Center" />
        </Grid
>

Here's what they look like after they've been moved a bit from their initial positions:

sis3.gif

Custom Controls

If you're creating controls that need only be used for special purposes in your own applications, the easiest approach is UserControl. Simply define a visual tree for the control in the XAML file.

You can also take a similar approach with ContentControl, except that the XAML file would contain a Style and ControlTemplate definition. The advantage of this approach is that you retain use of the Content property for the control's own purposes.

You can also derive from Control. This approach makes sense if the derived class is in a library, and you want the control to have a replaceable template. The default theme Style and ControlTemplate are in the library's generic.xaml file.

The program is tested in a project named WorldMap. The content area contains an XYSlider with the PlaneBackground property set to an ImageBrush based on a map of the world:

        <
Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>           
           
<petzold:XYSlider Name="xySlider"
                              Grid.Row="0"
                              ValueChanged="OnXYSliderValueChanged">
                <petzold:XYSlider.PlaneBackground>
                    <!-- Image courtesy of NASA/JPL-Caltech (http://maps.jpl.nasa.gov). -->
                    <ImageBrush ImageSource="Images/ear0xuu2.jpg" />
                </petzold:XYSlider.PlaneBackground>
            </petzold:XYSlider>           
           
<TextBlock Name="txtblk"
                       Grid.Row="1"
                       HorizontalAlignment="Center" />
        </Grid>

The code-behind file is devoted to handling the ValueChanged event from the XYSlider and converting the normalized Point to longitude and latitude:

namespace WorldMap
{
    public partial class MainPage :
PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
            DisplayCoordinates(xySlider.Value);
        } 
        void OnXYSliderValueChanged(object sender,
                                    RoutedPropertyChangedEventArgs<Point> args)
        {
            DisplayCoordinates(args.NewValue);
        } 
        void DisplayCoordinates(Point point)
        {
            double longitude = 360 * point.X - 180;
            double latitude = 90 - 180 * point.Y;
            txtblk.Text = String.Format("Longitude: {0:F0} Latitude: {1:F0}",
                                        longitude, latitude);
        }
    }
}

And here it is:

siss4.gif

As you move the Thumb with your finger, the longitude and latitude values displayed at the bottom are updated. It's easy to imagine the WorldMap program being enhanced to obtain the phone's location and using that to initialize the position of the Thumb.


Similar Articles