Custom Control Classes and Dependency Properties in Silverlight for 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

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:

el1.gif

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)
        {
            (obj
asColorColumn).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:

el2.gif

I deliberately designed the layout of SelectTwoColors so it wouldn't work quite well in portrait mode:

el3.gif


Similar Articles