Toggle Button and Panel with Attached 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've probably noticed a new style of toggle button in some Windows Phone 7 screens. Here they are on the page that lets you set date and time, blown up to almost double size:

et1.gif

If you experiment with these controls a bit, you'll find that you can toggle the switch just by tapping it, but you can also move the larger block back and forth with your finger, although it will tend to snap into position either at the left or right.

I'm not going to try to duplicate that more complex movement. My version will respond only to taps. For that reason I call it TapSlideToggle. The button is a UserControl derivative in the Petzold.Phone.Silverlight library. (I should note that something similar could be implemented entirely in a template applied to the existing ToggleButton, and the Silverlight for Windows Phone Toolkit implements this control under the name ToggleSwitchButton .) Here's the complete XAML file of my version:

<UserControl x:Class="Petzold.Phone.Silverlight.TapSlideToggle"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             d:DesignHeight="36" d:DesignWidth="96"> 
    <Grid x:Name="LayoutRoot"
          Background="Transparent"
          Width="96" Height
="36">
       
       
<Border BorderBrush="{StaticResource PhoneForegroundBrush}"
                BorderThickness="2"
                Margin="4 2"
                Padding
="4">
            <Rectangle Name="fillRectangle"
                       Fill="{StaticResource PhoneAccentBrush}"
                       Visibility
="Collapsed" />
        </Border> 
        <Border Name="slideBorder"
                BorderBrush="{StaticResource PhoneBackgroundBrush}"
                BorderThickness="4 0"
                HorizontalAlignment
="Left">
            <Rectangle Stroke="{StaticResource PhoneForegroundBrush}"
                       Fill="White"
                       StrokeThickness="2"
                       Width
="20" />
        </Border>
    </Grid>
</
UserControl>

To somewhat mimic the normal ToggleButton (but without the three-state option) the code-behind file defines an IsChecked dependency property of type bool and two events named Checked and Unchecked. One or the other of these events is fired when the IsChecked property changes value:

namespace Petzold.Phone.Silverlight
{
    public partial class TapSlideToggle : UserControl
    {
        public static readonly DependencyProperty IsCheckedProperty =
            DependencyProperty.Register("IsChecked",
                typeof(bool),
                typeof(TapSlideToggle),
                new PropertyMetadata(false, OnIsCheckedChanged)); 
        public event RoutedEventHandler Checked;
        public event RoutedEventHandler Unchecked; 
        public TapSlideToggle()
        {
            InitializeComponent();
        } 
        public bool IsChecked
        {
            set { SetValue(IsCheckedProperty, value); }
            get { return (bool)GetValue(IsCheckedProperty); }
        }          
        static void OnIsCheckedChanged(DependencyObject obj,
                                       DependencyPropertyChangedEventArgs args)
        {
            (obj as TapSlideToggle).OnIsCheckedChanged(args);           
        } 
        void OnIsCheckedChanged(DependencyPropertyChangedEventArgs args)
        {
            fillRectangle.Visibility = IsChecked ? Visibility.Visible :
                                                   Visibility.Collapsed; 
            slideBorder.HorizontalAlignment = IsChecked ? HorizontalAlignment.Right :
                                                          HorizontalAlignment.Left;
            if (IsChecked && Checked != null)
                Checked(this, new RoutedEventArgs());
            if (!IsChecked && Unchecked != null)
                Unchecked(this, new RoutedEventArgs());
        }
    }
}

The static property-changed handler calls an instance handler of the same name, which alters the visuals in the XAML just a little bit and then fires one of the two events. The only methods missing from the code above are the overrides of two Manipulation events. Here they are:

protected override void OnManipulationStarted(ManipulationStartedEventArgs args)
{
    args.Handled =
true;
    base.OnManipulationStarted(args);
}
protected override void OnManipulationCompleted(ManipulationCompletedEventArgs args)
{
    Point pt = args.ManipulationOrigin;
    if (pt.X > 0 && pt.X < this.ActualWidth &&
    pt.Y > 0 && pt.Y <
this.ActualHeight)
    IsChecked ^=
true;
    args.Handled =
true;
    base.OnManipulationCompleted(args);
}

The TapSlideToggleDemo program tests it out. The content area defines two instances of TapSlideToggle and two TextBlock element to display their current state:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <
Grid.RowDefinitions>
        <
RowDefinition Height="Auto" />
        <
RowDefinition Height="Auto" />
    </
Grid.RowDefinitions>
    <
Grid.ColumnDefinitions>
        <
ColumnDefinition Width="*" />
        <
ColumnDefinition Width="*" />
    </
Grid.ColumnDefinitions>
    <
TextBlock Name="option1TextBlock"
               Grid.Row="0" Grid.Column="0"
               Text="off"
               Margin="48"
               VerticalAlignment="Center" />
    <
petzold:TapSlideToggle Name="slideToggle1"
                            Grid.Row="0" Grid.Column="1"
                            Margin="48"
                            HorizontalAlignment="Right"
                            Checked="OnSlideToggle1Checked"
                            Unchecked="OnSlideToggle1Checked" />
    <
TextBlock Name="option2TextBlock"
               Grid.Row="1" Grid.Column="0"
               Text="off"
               Margin="48"
               VerticalAlignment="Center" />
    <
petzold:TapSlideToggle Name="slideToggle2"
                            Grid.Row="1" Grid.Column="1"
                            Margin="48"
                            HorizontalAlignment="Right"
                            Checked="OnSlideToggle2Checked"
                            Unchecked="OnSlideToggle2Checked" />
</
Grid>

Each of the two TapSlideToggle instances has both its Checked and Unchecked events set to the same handler, but different handlers are used for the two instances. This allows each handler to determine the state of the button by obtaining the IsChecked property and accessing the corresponding TextBlock:

namespace TapSlideToggleDemo
{
    public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
            slideToggle2.IsChecked = true;
        } 
        void OnSlideToggle1Checked(object sender, RoutedEventArgs args)
        {
            TapSlideToggle toggle = sender as TapSlideToggle;
            option1TextBlock.Text = toggle.IsChecked ? "on" : "off";
        } 
        void OnSlideToggle2Checked(object sender, RoutedEventArgs args)
        {
            TapSlideToggle toggle = sender as TapSlideToggle;
            option2TextBlock.Text = toggle.IsChecked ? "on" : "off";
        }
    }
}

And here's the result:

et2.gif

Panels with Properties

The Windows Presentation Foundation has a panel I often find useful called UniformGrid. As the name suggests, the UniformGrid divides its area into cells, each of which has the same dimensions.

By default, UniformGrid automatically determines a number of rows and columns by taking the ceiling of the square root of the number of children. For example, if there are 20 children, UniformGrid calculates 5 rows and columns (even though it might make more sense to have 5 rows and 4 columns, or 4 rows and 5 columns). You can override this calculation by explicitly setting the Rows or Columns property of UniformGrid to a non-zero number.

My version of UniformGrid is called UniformStack. It doesn't have a Rows or Columns property but it does have an Orientation property—the same property defined by StackPanel-to indicate whether the children of the panel will be arranged vertically or horizontally.

Here's the portion of the UniformStack class that defines the single dependency property and the property-changed handler:

public class UniformStack : Panel
{
    public static readonly DependencyProperty OrientationProperty =
    DependencyProperty.Register("Orientation",
    typeof(Orientation),
    typeof(UniformStack),
    new PropertyMetadata(Orientation.Vertical, OnOrientationChanged));
    public Orientation Orientation
    {
        set { SetValue(OrientationProperty, value); }
        get { return (Orientation)GetValue(OrientationProperty); }
    }
    static void OnOrientationChanged(DependencyObject obj,
    DependencyPropertyChangedEventArgs args)
    {
        (obj
as UniformStack).InvalidateMeasure();
    }
    ....
}

Well, it's not entirely clear. Certainly the panel has no choice but to offer to each child a Width of infinity. After that, one reasonable solution is to return a size from MeasureOverride with a Width that is five times the Width of the widest child.

That's what I do here:

protected
override Size MeasureOverride(Size availableSize)
{
    if (Children.Count == 0)
        return new Size();
        Size availableChildSize = new Size();
        Size
maxChildSize = new Size();
        Size compositeSize = new Size();

    // Calculate an available size for each child
    if (Orientation == Orientation.Horizontal)
        availableChildSize =
new Size(availableSize.Width / Children.Count,
                                        availableSize.Height);
    else
        availableChildSize = new Size(availableSize.Width,
                                        availableSize.Height / Children.Count);

    // Enumerate the children, and find the widest width and the highest height
    foreach (UIElement child in Children)
    {
        child.Measure(availableChildSize);
        maxChildSize.Width =
Math.Max(maxChildSize.Width, child.DesiredSize.Width);
        maxChildSize.Height =
Math.Max(maxChildSize.Height, child.DesiredSize.Height);
    }

    // Now determine a composite size that depends on infinite available width or height
    if (Orientation == Orientation.Horizontal)
    {
        if (Double.IsPositiveInfinity(availableSize.Width))
            compositeSize =
new Size(maxChildSize.Width * Children.Count,
                                        maxChildSize.Height);
        else
            compositeSize = new Size(availableSize.Width, maxChildSize.Height);
    }
    else
    {
        if (Double.IsPositiveInfinity(availableSize.Height))
                compositeSize =
new Size(maxChildSize.Width,
                                            maxChildSize.Height * Children.Count);
        else
            compositeSize = new Size(maxChildSize.Width, availableSize.Height);
    }
    return compositeSize;
}

The method begins by diving out if the panel has no children; this avoids division by zero later on.

The ArrangeOverride method calls Arrange on each child with the same size (called finalChildSize in the method) but with different x and y positions relative to the panel depending on orientation:

protected override Size ArrangeOverride(Size finalSize)
{
    if (Children.Count > 0)
    {
        Size finalChildSize = new Size();
        double x = 0;
        double y = 0;

        if (Orientation == Orientation.Horizontal)
            finalChildSize =
new Size(finalSize.Width / Children.Count,
                                        finalSize.Height);
        else
            finalChildSize = new Size(finalSize.Width,
                                        finalSize.Height / Children.Count);
        foreach (UIElement child in Children)
        {
            child.Arrange(
new Rect(new Point(x, y), finalChildSize));
            if (Orientation == Orientation.Horizontal)
                x += finalChildSize.Width;
            else
                y += finalChildSize.Height;
        }
    }
    return base.ArrangeOverride(finalSize);
}

Let's use the UniformStack to make a bar chart!

The QuickBarChart program actually uses three UniformStack panels:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <
petzold:UniformStack Orientation="Vertical">
        <
petzold:UniformStack x:Name="barChartPanel"
                              Orientation="Horizontal" />
        <
petzold:UniformStack Orientation="Horizontal">
            <
Button Content="Add 10 Items"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Click="OnButtonClick" />
            <
TextBlock Name="txtblk"
                       Text="0"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center" />
        </
petzold:UniformStack>
    </
petzold:UniformStack>
</
Grid>

The first UniformStack with a Vertical orientation simply divides the content area into two equal areas. (See how much easier it is to use than a regular Grid?) The top half contains another UniformStack with nothing in it (yet). The bottom one contains a UniformStack with a Horizontal orientation for a Button and a TextBlock.

Clicking the Button causes the code-behind file to add 10 more Rectangle elements to the UniformStack named barChartPanel:

namespace QuickBarChart
{
    public partial class MainPage : PhoneApplicationPage
    {
        Random rand = new Random(); 
        public MainPage()
        {
            InitializeComponent();
        } 
        void OnButtonClick(object sender, RoutedEventArgs args)
        {
            for (int i = 0; i < 10; i++)
            {
                Rectangle rect = new Rectangle();
                rect.Fill = this.Resources["PhoneAccentBrush"] as Brush;
                rect.VerticalAlignment = VerticalAlignment.Bottom;
                rect.Height = barChartPanel.ActualHeight * rand.NextDouble();
                rect.Margin = new Thickness(0, 0, 0.5, 0); 
                barChartPanel.Children.Add(rect);
            } 
            txtblk.Text = barChartPanel.Children.Count.ToString();
        }
    }
}

Notice that each Rectangle has a little half-pixel Margin on the right so there's at least some spacing between the bars. Still, I think you'll be surprised how many you can put in there before the display logic gives up:

et3.gif

Attached Properties

You now know almost everything you need to define your own attached properties. The project named CanvasCloneDemo contains a class named CanvasClone. The class defines two DependencyProperty fields named LeftProperty and TopProperty:

public class CanvasClone : Panel
{
    public static readonly DependencyProperty LeftProperty =
            DependencyProperty.RegisterAttached("Left",
                        typeof(double),
                        typeof(CanvasClone),
                        new PropertyMetadata(0.0, OnLeftOrTopPropertyChanged));

    public static readonly DependencyProperty TopProperty =
            DependencyProperty.RegisterAttached("Top",
                        typeof(double),
                        typeof(CanvasClone),
                        new PropertyMetadata(0.0, OnLeftOrTopPropertyChanged));
    ....
}

After defining the DependencyProperty fields, you need static methods to access the attached properties. These method names begin with Set and Get followed by the attached property names, in this case, Left and Top,

public static void SetLeft(DependencyObject obj, double value)
{
    obj.SetValue(LeftProperty, value);
}
public static double GetLeft(DependencyObject obj)
{
    return (double)obj.GetValue(LeftProperty);
}
public static void SetTop(DependencyObject obj, double value)
{
    obj.SetValue(TopProperty, value);
}
public static double GetTop(DependencyObject obj)
{
    return (double)obj.GetValue(TopProperty);
}

These methods get called either explicitly from code or implicitly from the XAML parser. The first argument will be the object on which the attached property is being set—in other words, the first argument will probably be a child of CanvasClone. The body of the method uses that argument to call SetValue and GetValue on the child. These are the same methods defined by DependencyObject to set and get dependency properties.

The XAML file in CanvasCloneDemo is the same as the one in the EllipseChain except that Canvas has been replaced with CanvasClone:

<
Grid x:Name="ContentPanel" Grid.Row="1">
    <
local:CanvasClone>
        <
local:CanvasClone.Resources>
            <
Style x:Key="ellipseStyle"
                   TargetType="Ellipse">
                <
Setter Property="Width" Value="100" />
                <
Setter Property="Height" Value="100" />
                <
Setter Property="Stroke" Value="{StaticResource PhoneAccentBrush}" />
                <
Setter Property="StrokeThickness" Value="10" />
            </
Style>
        </
local:CanvasClone.Resources>

        <
Ellipse Style="{StaticResource ellipseStyle}"
                 local:CanvasClone.Left="0" local:CanvasClone.Top="0" />

        <
Ellipse Style="{StaticResource ellipseStyle}"
                 local:CanvasClone.Left="52" local:CanvasClone.Top="53" />

        <
Ellipse Style="{StaticResource ellipseStyle}"
                 local:CanvasClone.Left="116" local:CanvasClone.Top="92" />

        <
Ellipse Style="{StaticResource ellipseStyle}"
                 local:CanvasClone.Left="190" local:CanvasClone.Top="107" />

        <
Ellipse Style="{StaticResource ellipseStyle}"
                 local:CanvasClone.Left="263" local:CanvasClone.Top="92" />

        <
Ellipse Style="{StaticResource ellipseStyle}"
                 local:CanvasClone.Left="326" local:CanvasClone.Top="53" />

        <
Ellipse Style="{StaticResource ellipseStyle}"
                 local:CanvasClone.Left="380" local:CanvasClone.Top="0" />
    </
local:CanvasClone>
</
Grid>

With much elation, we discover that the display looks the same as the earlier program:

et4.gif


Similar Articles