This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
you're creating a custom control class, and that class defines new properties, and if you want those properties to be set through styles, or you want those properties to be set through data bindings, or you want those properties to be the target of animations, then you need to do something very special with those properties, You need to make them
dependency properties.
To illustrate the difference that dependency properties make, let's first look at a custom control class perhaps coded by a naive programmer
Suppose you want to use a bunch of buttons whose foregrounds are colored with
various linear gradient brushes, and you figure it would be convenient for you
to specify the two colors as properties of the buttons, perhaps properties named
Color1 and Color2.
So you open a project named NaiveGradientButtonDemo and add a new class named NaiveGradientButton.
Here's that class:
public classNaiveGradientButton
: Button
{
GradientStop gradientStop1, gradientStop2;
public
NaiveGradientButton()
{
LinearGradientBrush brush = newLinearGradientBrush();
brush.StartPoint =newPoint(0,
0);
brush.EndPoint = newPoint(1,
0);
gradientStop1 = newGradientStop();
gradientStop1.Offset = 0;
brush.GradientStops.Add(gradientStop1);
gradientStop2 = newGradientStop();
gradientStop2.Offset = 1;
brush.GradientStops.Add(gradientStop2);
Foreground = brush;
}
publicColor
Color1
{
set { gradientStop1.Color = value;
}
get { return
(Color)gradientStop1.Color;
}
}
publicColor
Color2
{
set { gradientStop2.Color = value;
}
get { return
(Color)gradientStop2.Color;
}
}
}
Where did these two methods SetValue and GetValue come from? SetValue and
GetValue are two public methods defined by DependencyObject and inherited by all
derived classes. Notice that the second argument to SetValue is the value to
which the property is being set. The return value of GetValue is of type object
so it must be explicitly cast to a Color.
The MainPage.xaml file in the NaiveGradientButtonDemo project references this
class. The root element includes an XML namespace declaration that associates
the namespace prefix "local" with the CLR namespace of NaiveGradientButton:
xmlns:local="clr-namespace:NaiveGradientButtonDemo"
The Resources collection in MainPage.xaml defines a Style for
NaiveGradientButton:
<phone:PhoneApplicationPage.Resources>
<Style x:Key="gradientButtonStyle"
TargetType="local:NaiveGradientButton">
<Setter Property="HorizontalAlignment" Value="Center" />
<!--
<Setter Property="Color1" Value="Cyan" />
<Setter Property="Color2" Value="Pink" />
-->
</Style>
</phone:PhoneApplicationPage.Resources>
The content area of the XAML file has four instances of NaiveGradientButton
with their Color1 and Color2 properties set in a variety of different ways:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel>
<local:NaiveGradientButton Content="Naive Gradient Button #1"
HorizontalAlignment="Center" />
<local:NaiveGradientButton Content="Naive Gradient Button #2"
Color1="Blue" Color2="Red"
HorizontalAlignment="Center" />
<local:NaiveGradientButton Content="Naive Gradient Button #3"
Color1="{StaticResource PhoneForegroundColor}"
Color2="{StaticResource PhoneBackgroundColor}"
HorizontalAlignment="Center" />
<local:NaiveGradientButton Content="Naive Gradient Button #4"
Style="{StaticResource gradientButtonStyle}" />
</StackPanel>
</Grid>
When you run the program, you'll discover that the
second and third buttons are fine, but the first and fourth seem to have no
content:
In Silverlight, properties can be set in several ways. We have empirically
discovered that a strict precedence is established when the same property is set
from property inheritance, or from a theme, or a style, or a local setting.
Deriving from UserControl
As you've seen, it's possible to derive from a class that derives from
Control to add some additional properties. It's also possible to derive directly
from Control to create entirely new controls (or to derive from ContentControl
if the control needs to have a Content property). However, deriving from Control
or ContentControl in a proper and cordial manner involves creating a default
template in XAML that describes the control's visual appearance, and allowing
that template to be replaced to redefine the visuals of the control.
Within the Petzold.Phone.Silverlight project given below (or any other
library project), you can add a new item by right-clicking the project name in
the Solution Explorer and selecting Add and New Item.
To make a new UserControl in either an application project or a
library project. From the Add New Item dialog box, select Windows Phone User
Control and give it a name.You'll get two files: a XAML file and a code-behind
file.
The XAML file is rather simpler than the one created for a
PhoneApplicationPage class. The root element is UserControl.
It contains an x:Class attribute indicating the derived class, and the only
nested element is a Grid
named LayoutRoot.
You don't need to retain that Grid but it's usually convenient.
I also deleted the designer-related attributes, so here's the complete ColorColumn.xaml file. Notice I've also changed the Background property on the Grid from a StaticResource referencing PhoneChromeBrush to Transparent:
<UserControl
x:Class="Petzold.Phone.Silverlight.ColorColumn"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup
compatibility/2006" d:DesignHeight="800" d:DesignWidth="480">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Name="colorLabel"
Grid.Row="0"
TextAlignment="Center" />
<Slider Name="slider"
Grid.Row="1"
Orientation="Vertical"
Minimum="0"
Maximum="255"
ValueChanged="OnSliderValueChanged" />
<TextBlock Name="colorValue"
Grid.Row="2"
Text="00"
TextAlignment="Center" />
</Grid>
</UserControl>
Generally a
UserControl
derivative will define its own properties and events, and
very often these properties and events will parallel properties and events of
elements in its visual tree. It's typical for a class like ColorColumn to
have a Label property corresponding to the Text property
of a TextBlock,
and a Value property corresponding to the Value property
of the Slider,
and a ValueChanged event corresponding to the ValueChanged
event of the Slider.
Here's the portion of the
ColorColumn code-behind file devoted to the Label property
for the text above the Slider:
public partialclassColorColumn
: UserControl
{
...
public staticreadonlyDependencyProperty
LabelProperty =
DependencyProperty.Register("Label",
typeof(string),
typeof(ColorColumn),
new PropertyMetadata(OnLabelChanged));
...
public string
Label
{
set { SetValue(LabelProperty, value);
}
get { return
(string)GetValue(LabelProperty);
}
}
...
static void
OnLabelChanged(DependencyObject
obj,
DependencyPropertyChangedEventArgs args)
{
(objasColorColumn).colorLabel.Text
= args.NewValue asstring;
}
}
The Value property in
ColorColumn is a little more complex because it needs to
fire a ValueChanged event. This
Value property is eventually used in the calculation of a
Color,
so I thought it should be of type byte rather
than double.
Here's the code in the class pertaining to the Value property
and ValueChanged event:
publicpartial classColorColumn : UserControl
{
public staticreadonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value",
typeof(byte),
typeof(ColorColumn),
new PropertyMetadata((byte)0, OnValueChanged));
....
public eventRoutedPropertyChangedEventHandler<byte> ValueChanged;
....
public byte Value
{
set { SetValue(ValueProperty,value); }
get { return (byte)GetValue(ValueProperty); }
}
....
static void OnValueChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
(obj as ColorColumn).OnValueChanged((byte)args.OldValue, (byte)args.NewValue);
}
protected virtual void OnValueChanged(byte oldValue, byte newValue)
{
slider.Value = newValue;
colorValue.Text = newValue.ToString("X2");
if (ValueChanged !=null)
ValueChanged(this,
newRoutedPropertyChangedEventArgs<byte>(oldValue, newValue));
}
....
}
The only code of ColorColumn not yet discussed encompass the constructor and
the handler for the ValueChanged event of the Slider. This event handler simply
casts the Value property of the Slider to a byte and sets it to the Value
property of the ColorColumn class.
publicpartial classColorColumn : UserControl
{
....
public ColorColumn()
{
InitializeComponent();
}
....
void OnSliderValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> args)
{
Value = (byte)args.NewValue;
}
}
In reality, this doesn't happen because at some point one of these Value
properties-either the Value property of the Slider or the Value property of
ColorColumn-will be set to its
existing value, and no property-changed event will be fired. The infinite loop
grinds to a halt.
The
RgbColorScoller class also derives from UserControl and consists of three
ColorColumn controls. Here's the complete XAML file:
<UserControl
x:Class="Petzold.Phone.Silverlight.RgbColorScroller"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<petzold:ColorColumn x:Name="redColumn"
Grid.Column="0"
Foreground="Red"
Label="Red"
ValueChanged="OnColorColumnValueChanged" />
<petzold:ColorColumn x:Name="greenColumn"
Grid.Column="1"
Foreground="Green"
Label="Green"
ValueChanged="OnColorColumnValueChanged" />
<petzold:ColorColumn x:Name="blueColumn"
Grid.Column="2"
Foreground="Blue"
Label="Blue"
ValueChanged="OnColorColumnValueChanged" />
</Grid>
</UserControl>
The RgbColorScroller class defines one property named Color (of type Color, of course) and an event named ColorChanged. Here's the whole class in one shot:
namespace Petzold.Phone.Silverlight
{
public partialclass RgbColorScroller : UserControl
{
public static readonlyDependencyProperty ColorProperty =
DependencyProperty.Register("Color",
typeof(Color),
typeof(RgbColorScroller),
newPropertyMetadata(Colors.Gray, OnColorChanged));
public event RoutedPropertyChangedEventHandler<Color> ColorChanged;
public RgbColorScroller()
{
InitializeComponent();
}
public Color Color
{
set { SetValue(ColorProperty,value); }
get { return (Color)GetValue(ColorProperty); }
}
void OnColorColumnValueChanged(object sender,
RoutedPropertyChangedEventArgs<byte> args)
{
Color = Color.FromArgb(255, redColumn.Value,
greenColumn.Value,
blueColumn.Value);
}
static void OnColorChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
(obj asRgbColorScroller).OnColorChanged((Color)args.OldValue,
(Color)args.NewValue);
}
protected virtual void OnColorChanged(Color oldValue, Color newValue)
{
redColumn.Value = newValue.R;
greenColumn.Value = newValue.G;
blueColumn.Value = newValue.B;
if (ColorChanged !=null)
ColorChanged(this,
newRoutedPropertyChangedEventArgs<Color>(oldValue, newValue));
}
}
}
To use this RgbColorScroller class
from the Petzold.Phone.Silverlight library, create a new application project.
Let's call it SelectTwoColors. Right-click the References header under the
project name in the Solution Explorer, and select Add Reference. In the Add
Reference dialog box, select the Browse tag. Navigate to the DLL file (in this
case Petzold.Phone.Silverlight.dll) and select it.
In the MainPage.xaml file you'll need a XML namespace
declaration for the library. Because the library is a separate assembly, this
namespace declaration requires an assembly section to refer to the DLL file:
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight;assembly=Petzold.Phone.Silverlight"
The SelectTwoColors XAML file has two RgbColorScroller controls, each inside
a Border with a Rectangle element between them. Each RgbColorScroll has its
ColorChanged event attached to the same handler:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*"
/>
<ColumnDefinition
Width="*"
/>
<ColumnDefinition
Width="*"
/>
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
BorderBrush="{StaticResource
PhoneForegroundBrush}"
BorderThickness="2"
Margin="12"
Padding="12">
<petzold:RgbColorScroller
Name="colorScroller1"
ColorChanged="OnColorScrollerColorChanged"
/>
</Border>
<Rectangle
Name="rectangle"
Grid.Column="1"
StrokeThickness="24"
Margin="12"
/>
<Border
Grid.Column="2"
BorderBrush="{StaticResource
PhoneForegroundBrush}"
BorderThickness="2"
Margin="12"
Padding="12">
<petzold:RgbColorScroller
FontSize="12"
Name="colorScroller2"
ColorChanged="OnColorScrollerColorChanged"
/>
</Border>
</Grid>
The constructor of the code-behind file initializes the two RgbColorScroller
controls with two colors, which causes the first ColorChanged events to fire,
which are then processed by the event handler to set colors on the Rectangle:
namespace SelectTwoColors
{
public partialclass MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
colorScroller1.Color = Color.FromArgb(0xFF, 0xC0, 0x80, 0x40);
colorScroller2.Color = Color.FromArgb(0xFF, 0x40, 0x80, 0xC0);
}
void OnColorScrollerColorChanged(object sender,
RoutedPropertyChangedEventArgs<Color> args)
{
Brush brush =new SolidColorBrush(args.NewValue);
if (sender == colorScroller1)
rectangle.Stroke = brush;
else if (sender == colorScroller2)
rectangle.Fill = brush;
}
}
}
And here it is in landscape mode:
I deliberately designed the layout of SelectTwoColors so it wouldn't work
quite well in portrait mode: