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:
The CheckedContent property uses Botticelli's Birth of Venus:
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:
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:
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.