Welcome to MSDN Blogs Sign in | Join | Help

Retemplating a Standard Control (Including VisualStateManager Stuff)

This example demonstrates how to give a Button new visuals and visual state by re-templating. It also includes what to do with all of the existing VisualStateManager stuff, but first it discusses various ways of modifying existing controls.

There are a few different ways of modifying an existing Control, with varying degrees of depth and difficulty. Let's take a quick look at them.

Simple tweaks to a few controls

It is possible to modify the properties of a control to change its appearance. You will be constrained by the existing visual design, but you will be able to tweak things a bit. This is as simple as changing the Background and font properties on a Button:

<Button Content="Button" Background="Green" FontFamily="Verdana" FontSize="20" Width="100" Height="30"/>

image

This is fine if you don't have to do it very often. But imagine if you have set the FontFamily and FontSize of all of the Buttons on your page, and you want to change them. It would be nice not to have to change them everywhere.

Common value for a property used in multiple controls

You can use application-wide values to centralize the control of your common parameters. One way to do that is to use resources. I have added the "commonFontSize" resource to my application's Resources section:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             x:Class="RetemplatedButtonApp.App">
    <Application.Resources>
        <sys:Double x:Name="commonFontSize">20</sys:Double>
    </Application.Resources>
</Application>

The FontSize property is of type Double, so that's what I've created. I can use it like this:

<Button Content="Button" Background="Green" FontFamily="Verdana" FontSize="{StaticResource commonFontSize}" Width="100" Height="30"/>

The Button will look the same. The StaticResource reference will look for a resource named "commonFontSize" and attempt to set the FontSize property to it. Notice how the "sys" xmlns prefix was added so that an object of type Double could be created.

Common values for lots of properties on lots of controls

Let's say that you want all of the Buttons in your app to have the same look, and there are lots of properties that you want to set. Setting them all to their values is a maintenance nightmare, and even using resources to centralize the actual values is painful, because you have to set a bunch of properties on each Button.

One way to make this easy is to use Styles. This centralizes not only the values but setting the properties. Here's an example of a Style that can be applied to Borders:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="RetemplatedButtonApp.App">
    <Application.Resources>
        <Style x:Name="commonBorderStyle" TargetType="Border">
            <Setter Property="Margin" Value="2"/>
            <Setter Property="BorderThickness" Value="4"/>
            <Setter Property="CornerRadius" Value="4"/>
            <Setter Property="BorderBrush" Value="Black"/>
            <Setter Property="Background">
                <Setter.Value>
                    <LinearGradientBrush StartPoint="0.7,0" EndPoint="0.7,1">
                        <GradientStop Color="White" Offset="0" />
                        <GradientStop Color="Red" Offset="0.35" />
                        <GradientStop Color="Blue" Offset="0.35" />
                        <GradientStop Color="White" Offset="1" />
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>
</Application>

To use this style, it must be set in each Border:

<Border Style="{StaticResource commonBorderStyle}" Width="100" Height="50"/>

This produces this Border that is so lovely that it will undoubtedly soon be seen in all of the coolest Silverlight apps. Well, maybe not, but it was a useful example.

image

Note that for properties that have types that can be set with a simple string value converter, the Value is set as an attribute. For other properties, the property element syntax can be used. The Background property is an example of this, because a LinearGradientBrush can't be defined with a string, the way a SolidColorBrush can be (see the BorderBrush property in just above the Background.)

Your Style could also use Resources:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="RetemplatedButtonApp.App">
    <Application.Resources>
        <LinearGradientBrush x:Name="bkBrush" StartPoint="0.7,0" EndPoint="0.7,1">
            <GradientStop Color="White" Offset="0" />
            <GradientStop Color="Red" Offset="0.35" />
            <GradientStop Color="Blue" Offset="0.35" />
            <GradientStop Color="White" Offset="1" />
        </LinearGradientBrush>
        <Style x:Name="commonBorderStyle" TargetType="Border">
            <Setter Property="Margin" Value="2"/>
            <Setter Property="BorderThickness" Value="4"/>
            <Setter Property="CornerRadius" Value="4"/>
            <Setter Property="BorderBrush" Value="Black"/>
            <Setter Property="Background" Value="{StaticResource bkBrush}"/>
        </Style>
    </Application.Resources>
</Application>

Subclassing a control

It is easy to subclass controls in Silverlight. Let's say that you want to keep a control the way it is, but add some additional PME (properties, methods and events) to it. For example, if you wanted to associate some data with a RadioButton, that was different from its Content, you could just use the Tag property. But if you wanted strongly-typed data, you could some properties.

Here's the code for a simple RadioButton subclass:

using System;
using System.Windows;
using System.Windows.Controls;

namespace RetemplatedButtonApp
{
    public class MyRadioButton : RadioButton
    {
        public MyRadioButton()
        {
            FontFamily = new System.Windows.Media.FontFamily("Verdana");
            FontSize = 20;
        }

        public int Count { get; set; }
    }
}

Note that this example sets some properties in the constructor. This is another way to set the properties of your elements, but if all you want to do is set some properties the same, styling is a better option. When you set properties like this, they can't be styled, because a locally-set value will override a styled value.

To use this in XAML, you have to add an xmlns prefix for the namespace, and then use that prefix to instantiate the element:

<UserControl x:Class="RetemplatedButtonApp.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:RetemplatedButtonApp">
    <Grid x:Name="LayoutRoot" Background="White">
        <Border BorderThickness="2" BorderBrush="Black" CornerRadius="8" HorizontalAlignment="Center" VerticalAlignment="Center" Padding="8">
            <StackPanel>
                <local:MyRadioButton Content="Once" Count="1"/>
                <local:MyRadioButton Content="Twice" Count="2"/>
                <local:MyRadioButton Content="Three times" Count="3"/>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>

This produces a Border with custom RadioButtons like this:

image

Subclassing with re-templating

The most complete overhaul of a control is to subclass it and re-template it. The re-templating is generally done by adding a generic.xaml file to your project, and putting a new default style in it. You will also need to define a DefaultStyleKey in your ctor. I won't do a full example of this, but here are the steps to ensure that your control will be templated with your new visuals. Note that putting a Style and ControlTemplate in generic.xaml doesn't do you any good unless you also subclass. This topic deserves a post by itself, but briefly, here are the steps:

  1. Create the file generic.xaml in your project.
  2. Make sure that its Build Action is set to "Resource". Nothing else will work.
  3. The root element of generic.xaml is a ResourceDictionary. It should have xmlns prefixes for all of the namespaces of the controls you are subclassing and retemplating.
  4. The children of the ResourceDictionary are Styles. Make sure that there is a Style with a TargetType set to the type of your subclassed control. You'll have to put the appropriate xmlns prefix on the type.
  5. One of the Setters in the Style should be a ControlTemplate. You'll have to set the TargetType on that, too.
  6. In your subclassed control's ctor, set the DefaultStyleKey to your subclassed type, otherwise it will get the built in style from the base class.

Re-templating without subclassing

If you want to overhaul the visuals of a control, and give it a new appearance when it changes state, you can retemplate without subclassing. In this example, we'll build a new ControlTemplate for a button. I want a bold Button with a circular border that I can put Paths into. I also want it to grow when the mouse is over it. But in all other respects, I want it to be an ordinary Button. This is how I envision it in use, as the button in the upper-right:

image

The first thing that I will do is create a project, and put a Button in it. Here's what the XAML will start off like:

<UserControl x:Class="EllipseButton.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid x:Name="LayoutRoot" Background="White">
        <Button Width="40" Height="40"/>
    </Grid>
</UserControl>

It doesn't look very elliptical yet:

image

The next thing to do is get the built-in Style from generic.xaml. I used Reflector [note: this link was dead when I tried it, but hopefully it is a temporary glitch], but David Anson also has a utility that you can use. Take the built-in style for your control, and add it to your XAML in your page's Resources. Make your existing Button use the new Style. I also added the xmlns prefix for "vsm". I'm omitting the contents of the Style of the template because it is pretty long, but here's the rest of it:

<UserControl x:Class="EllipseButton.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
    <UserControl.Resources>
        <Style x:Name="EllipseButtonStyle" TargetType="Button">
<!-- contents not shown --> </Style> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="White"> <Button Style="{StaticResource EllipseButtonStyle}" Width="40" Height="40"/> </Grid> </UserControl>

If you have done your typing and pasting correctly, you should get the exact same appearance when you run your app.

The next thing to do is to get rid of just about everything except for the root Grid, the VisualStateManager elements (get rid of the Storyboards) and the ContentPresenter. Your XAML will now look like this:

<UserControl x:Class="EllipseButton.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
    <UserControl.Resources>
        <Style x:Name="EllipseButtonStyle" TargetType="Button">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid>
                            <vsm:VisualStateManager.VisualStateGroups>
                                <vsm:VisualStateGroup x:Name="CommonStates">
                                    <vsm:VisualStateGroup.Transitions>
                                    </vsm:VisualStateGroup.Transitions>
                                    <vsm:VisualState x:Name="Normal" />
                                    <vsm:VisualState x:Name="MouseOver">
                                    </vsm:VisualState>
                                    <vsm:VisualState x:Name="Pressed">
                                    </vsm:VisualState>
                                    <vsm:VisualState x:Name="Disabled">
                                    </vsm:VisualState>
                                </vsm:VisualStateGroup>
                                <vsm:VisualStateGroup x:Name="FocusStates">
                                    <vsm:VisualState x:Name="Focused">
                                    </vsm:VisualState>
                                    <vsm:VisualState x:Name="Unfocused">
                                    </vsm:VisualState>
                                </vsm:VisualStateGroup>
                            </vsm:VisualStateManager.VisualStateGroups>

                            <ContentPresenter
                                  Content="{TemplateBinding Content}"
                                  ContentTemplate="{TemplateBinding ContentTemplate}"
                                  HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                                  Padding="{TemplateBinding Padding}"
                                  TextAlignment="{TemplateBinding TextAlignment}"
                                  TextDecorations="{TemplateBinding TextDecorations}"
                                  TextWrapping="{TemplateBinding TextWrapping}"
                                  VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                                  Margin="4,5,4,4"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <Button Style="{StaticResource EllipseButtonStyle}" Width="40" Height="40"/>
    </Grid>
</UserControl>

If you try to run your app now, you will get nothing on the screen, although if you give the Button some Content, you will see it. This isn't a very useful Button. If you click on the Content, the Click event will be raised, but the Button itself has no visuals, and it there is no visual response to user actions.

Let's leave the VisualStateManager stuff alone for right now, since we really need to do the visuals first. We'll add this Ellipse just before the ContentPresenter, so it will be drawn behind the ContentPresenter:

<Ellipse Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" StrokeThickness="2"/>

Let's also add an Ellipse after the ContentPresenter, so that it will be drawn on top. This will be used to wash the colors out when the Button is disabled, but we'll give it an Opacity of 0 for now:

<Ellipse x:Name="disabledEllipse" Fill="White" Opacity="0"/>

Let's also change the Button to give it some Content and a Background color:

<Button Style="{StaticResource EllipseButtonStyle}" Width="40" Height="40" Content="Hello" Background="#4b2"/>

If you don't want to hardcode values (such as colors) in your control, it is important that you use TemplateBindings wherever it is appropriate. In the Ellipse that we added, I've made sure that its Stroke and Fill will be set to whatever values are set on the control. Controls with hardcoded values are not as flexible and useful as controls that use TemplateBindings and properties to control appearance and/or behavior. If I wanted to be consistent with that principle, I might subclass Button so that I could also bind the Ellipse's StrokeThickness, but I don't want to subclass for this example.

Here's what it looks like now:

image

Now we're looking elliptical. You can see the Button, and it raises the Click event when you click on it, but it always looks the same. Time to start plugging StoryBoards into the VisualStates.

If you look at the VisualStateGroups and VisualStates, you can see how Button is organized. Internally, Button keeps track of a bunch of state, and then figures out which VisualState in each VisualStateGroup it should be in, and calls VisualStateManager.GoToState for each state. By putting Storyboards in the VisualStates, we can make this Button start to react to input.

The first VisualStateGroup is the "CommonStates" group; the first state in that is "Normal". By convention, the "Normal" is what the control will look like if it is enabled but is not being interacted with by the user. Its appearance is assumed to be the appearance as specified by the template XAML, so we have already defined the "Normal" state. Because of the way VisualStateManager uses the animation system, we don't need to specify any Storyboards for the "Normal" state.

The VisualStates Storyboards represent the steady state animations of the particular state. Usually, there aren't any steady state animations once a control is in a particular state, so all of the animations have their Duration properties set to 0. We want the control to expand when the user moves the mouse over it, so add a ScaleTransform to the root Grid, set the Grid's RenderTransformOrigin to .5,.5:

<Grid RenderTransformOrigin=".5,.5">
    <Grid.RenderTransform>
        <ScaleTransform x:Name="zoom"/>
    </Grid.RenderTransform>

and add a StoryBoard to the "MouseOver" state:

<vsm:VisualState x:Name="MouseOver">
    <Storyboard>
        <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleX" Duration="0"/>
        <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleY" Duration="0"/>
    </Storyboard>
</vsm:VisualState>

Now, when you run it, when you move the mouse over the button, it grows. When you click on it, it goes back down to its original size. This works really well if you don't move the mouse between pressing the button and releasing it, you can just keep clicking and everything is fine, but things get finicky if you move the mouse (after mouse down but before mouse up) into the region that is in the expanded size, but outside of the original size. You can't just keep clicking. That might be OK, but let's try to make things a bit nicer. What we want to do is to define the "Pressed" state. The Storyboards for the individual VisualStates should have no relation to each other. In other words, all other states can be ignored, and all you have to do is to add animations to set the values relative to the XAML in the template (which as you may remember is considered to be the "Normal" state.)

So copy the Storyboard that you added to the "MouseOver" state and paste it into the "Pressed" state. Now, when you click on the Button, it does not shrink, but neither is there any indication that something is happening. So let's add another ScaleTransform, to the ContentPresenter this time, and give it a RenderTransformOrigin, too (try leaving the RenderTransformOrigin off and seeing what happens):

<ContentPresenter
        Content="{TemplateBinding Content}"
        ContentTemplate="{TemplateBinding ContentTemplate}"
        HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
        Padding="{TemplateBinding Padding}"
        TextAlignment="{TemplateBinding TextAlignment}"
        TextDecorations="{TemplateBinding TextDecorations}"
        TextWrapping="{TemplateBinding TextWrapping}"
        VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
        Margin="4,5,4,4"
        RenderTransformOrigin=".5,.5">
    <ContentPresenter.RenderTransform>
        <ScaleTransform x:Name="contentZoom"/>
    </ContentPresenter.RenderTransform>
</ContentPresenter>

And let's add some more animations to the "MouseOver" state:

<vsm:VisualState x:Name="Pressed">
    <Storyboard>
        <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleX" Duration="0"/>
        <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleY" Duration="0"/>
        <DoubleAnimation To=".5" Storyboard.TargetName="contentZoom" Storyboard.TargetProperty="ScaleX" Duration="0"/>
        <DoubleAnimation To=".5" Storyboard.TargetName="contentZoom" Storyboard.TargetProperty="ScaleY" Duration="0"/>
    </Storyboard>
</vsm:VisualState>

Since we are doubling the size of the Button's visuals when we animate the "zoom" transform, if we halve them in the "Pressed" state by animating the "contentZoom" ScaleTransform, the Content will go back to its normal size and we'll get a nice press effect.

Let's also add a Storyboard for the "Disabled" state:

<vsm:VisualState x:Name="Disabled">
    <Storyboard>
        <DoubleAnimation To=".4" Storyboard.TargetName="disabledEllipse" Storyboard.TargetProperty="Opacity" Duration="0"/>
    </Storyboard>
</vsm:VisualState>

For the focus visuals, let's insert another Ellipse immediately before the ContentPresenter (so that it will not be drawn over the Content). It has a RadialGradient to give the Button a little glow when it is focused:

<Ellipse x:Name="focusEllipse" Opacity="0">
    <Ellipse.Fill>
        <RadialGradientBrush Center="0.5,0.4" GradientOrigin="0.25,0.4">
            <RadialGradientBrush.GradientStops>
                <GradientStop Color="White" Offset="0.0" />
                <GradientStop Color="Black" Offset="1" />
            </RadialGradientBrush.GradientStops>
        </RadialGradientBrush>
    </Ellipse.Fill>
</Ellipse>

Animate it in the "Focused" state just like the "Disabled" state:

<vsm:VisualState x:Name="Focused">
    <Storyboard>
        <DoubleAnimation To=".4" Storyboard.TargetName="focusEllipse" Storyboard.TargetProperty="Opacity" Duration="0"/>
    </Storyboard>
</vsm:VisualState>

We don't need to do anything in the "Unfocused" state, because the Opacity of the "focusEllipse" is set to 0 in the XAML.

So now we have a Button that does all the right things: it has nice mouse over and click effects, and can respond to being disabled and focused. The only problem is that the effects are rather jarring--they simply jump to their values. We should smooth things out. We can do this by adding a default VisualTransition in the CommonStates, like this:

<vsm:VisualStateGroup x:Name="CommonStates">
    <vsm:VisualStateGroup.Transitions>
        <VisualTransition Duration="0:0:0.1"/>
    </vsm:VisualStateGroup.Transitions>

This means that all transitions will take 0.1 seconds, which is quick enough to be responsive, but still smooth. Add a similar VisualTransition to the "FocusState" VisualStateGroup, and the focus visuals will also be smooth.

Here's our final XAML:

<UserControl x:Class="EllipseButton.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
    <UserControl.Resources>
        <Style x:Name="EllipseButtonStyle" TargetType="Button">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid RenderTransformOrigin=".5,.5">
                            <Grid.RenderTransform>
                                <ScaleTransform x:Name="zoom"/>
                            </Grid.RenderTransform>
                            <vsm:VisualStateManager.VisualStateGroups>
                                <vsm:VisualStateGroup x:Name="CommonStates">
                                    <vsm:VisualStateGroup.Transitions>
                                        <VisualTransition Duration="0:0:0.1"/>
                                    </vsm:VisualStateGroup.Transitions>
                                    <vsm:VisualState x:Name="Normal" />
                                    <vsm:VisualState x:Name="MouseOver">
                                        <Storyboard>
                                            <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleX" Duration="0"/>
                                            <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleY" Duration="0"/>
                                        </Storyboard>
                                    </vsm:VisualState>
                                    <vsm:VisualState x:Name="Pressed">
                                        <Storyboard>
                                            <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleX" Duration="0"/>
                                            <DoubleAnimation To="2" Storyboard.TargetName="zoom" Storyboard.TargetProperty="ScaleY" Duration="0"/>
                                            <DoubleAnimation To=".5" Storyboard.TargetName="contentZoom" Storyboard.TargetProperty="ScaleX" Duration="0"/>
                                            <DoubleAnimation To=".5" Storyboard.TargetName="contentZoom" Storyboard.TargetProperty="ScaleY" Duration="0"/>
                                        </Storyboard>
                                    </vsm:VisualState>
                                    <vsm:VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <DoubleAnimation To=".4" Storyboard.TargetName="disabledEllipse" Storyboard.TargetProperty="Opacity" Duration="0"/>
                                        </Storyboard>
                                    </vsm:VisualState>
                                </vsm:VisualStateGroup>
                                <vsm:VisualStateGroup x:Name="FocusStates">
                                    <vsm:VisualStateGroup.Transitions>
                                        <VisualTransition Duration="0:0:0.1"/>
                                    </vsm:VisualStateGroup.Transitions>
                                    <vsm:VisualState x:Name="Focused">
                                        <Storyboard>
                                            <DoubleAnimation To=".4" Storyboard.TargetName="focusEllipse" Storyboard.TargetProperty="Opacity" Duration="0"/>
                                        </Storyboard>
                                    </vsm:VisualState>
                                    <vsm:VisualState x:Name="Unfocused">
                                    </vsm:VisualState>
                                </vsm:VisualStateGroup>
                            </vsm:VisualStateManager.VisualStateGroups>

                            <Ellipse Fill="{TemplateBinding Background}" Stroke="{TemplateBinding Foreground}" StrokeThickness="2"/>

                            <Ellipse x:Name="focusEllipse" Opacity="0">
                                <Ellipse.Fill>
                                    <RadialGradientBrush Center="0.5,0.4" GradientOrigin="0.25,0.4">
                                        <RadialGradientBrush.GradientStops>
                                            <GradientStop Color="White" Offset="0.0" />
                                            <GradientStop Color="Black" Offset="1" />
                                        </RadialGradientBrush.GradientStops>
                                    </RadialGradientBrush>
                                </Ellipse.Fill>
                            </Ellipse>

                            <ContentPresenter
                                    Content="{TemplateBinding Content}"
                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                                    Padding="{TemplateBinding Padding}"
                                    TextAlignment="{TemplateBinding TextAlignment}"
                                    TextDecorations="{TemplateBinding TextDecorations}"
                                    TextWrapping="{TemplateBinding TextWrapping}"
                                    VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                                    Margin="4,5,4,4"
                                    RenderTransformOrigin=".5,.5">
                                <ContentPresenter.RenderTransform>
                                    <ScaleTransform x:Name="contentZoom"/>
                                </ContentPresenter.RenderTransform>
                            </ContentPresenter>
                            
                            <Ellipse x:Name="disabledEllipse" Fill="White" Opacity="0"/>
                        
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <Button Style="{StaticResource EllipseButtonStyle}" Width="40" Height="40" Content="Hello" Background="#4b2" Click="Button_Click"/>
    </Grid>
</UserControl>

Here's how the focused Button looks now (unfocused, it looks the same as above):

image

So here's basically what to do:

  1. Create a project with an instance of the control you want to retemplate
  2. Copy over the built-in style from generic.xaml
  3. Remove most of the visuals and all of the Storyboards from the ControlTemplate
  4. Add visuals
  5. Add Storyboards to the VisualStates

Now that you have a Style with this ControlTemplate, you can put it in your Application.Resource section for use throughout your application, or you could create a subclass for this control and put the Style in the generic.xaml of the control's assembly.

Posted by Dave Relyea | 2 Comments

Attachment(s): EllipseButton.zip

Silverlight Layout Fundamentals Part 2 - Layout Containers

In Layout Fundamentals Part 1, I started slowly, and demonstrated the need for a layout system. I touched on what it can do for you, and layout containers and properties. This post covers the layout containers and some layout concepts in a bit more detail. I will touch on some properties that affect layout; those will be examined in more depth in Part 3.

Canvas

Canvas, the most basic layout container, was present in Silverlight 1.0. It should not be used in most layout scenarios, as for the most part, it makes you do everything yourself. It lets its children be as big as they want to be, and positions them according to the Canvas.Left and Canvas.Top attached properties. It is "boundless" and does not clip by default. In Layout Fundamentals Part 1, the first examples showed how much "fun" it can be to position elements on a Canvas.

Border

Border is a very simple layout container. It does not derive from Panel, and can have only one child, which is centered by default. In addition to the Background property, Border has the BorderBrush, BorderThickness, CornerRadius, and Padding properties.

BorderBrush specifies the brush that will be used to draw the border of the Border. This is analogous to the Stroke property on a Rectangle. BorderThickness determines how many pixels thick each side of the Border will be, and CornerRadius determines the curve on each corner. The Padding property is used to put space between the border and the content. Note that the CornerRadius is used for rendering only. Only the BorderThickness property is used for layout calculations. This means that although it is possible to make a Border that looks like a circle, it is also possible to have the content overlapping the rounded corners of the border.

This XAML:

<Border Height="100" Width="150" BorderBrush="Blue" BorderThickness="12,4" CornerRadius="12,4,12,4" Background="LightBlue">
    <TextBlock Text="Fancy Border" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>

produces a fancy Border like this:

image

I said that the Border centers its children by default, but it isn't really the Border that does that, but rather the default values for HorizontalAlignment and VerticalAlignment on its child that do the centering. You'll notice that I specified the alignment of the TextBlock. TextBlock is special; it will take up all the space that is given to it, and render itself in the top-left, unless explicitly told to do otherwise. Also note that I specified the Width and Height of the Border. That is always an option, but you can let the Border figure out for itself how big it should be. In this case, it would first determine how big its child (the TextBlock) is, then add the Padding and BorderThickness properties.

<Border BorderBrush="Blue" BorderThickness="12,4" CornerRadius="12,4,12,4" Background="LightBlue">
    <TextBlock Text="Fancy Border" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>

image

The Border is just a bit too tight around the text, so let's add some Padding:

<Border BorderBrush="Blue" BorderThickness="12,4" CornerRadius="12,4,12,4" Background="LightBlue" Padding="4">
    <TextBlock Text="Fancy Border" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>

image

The Padding property is used in addition to the Margin property, which will be discussed below. If we put a Margin on the TextBlock, then we get this:

<Border BorderBrush="Blue" BorderThickness="12,4" CornerRadius="12,4,12,4" Background="LightBlue" Padding="4">
    <TextBlock Text="Fancy Border" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8"/>
</Border>

image

Now there are 12 pixels between the TextBlock and the inside of the Border. It doesn't really matter how many of those pixels are the Border's Padding, or the TextBlock's Margin.

And now, I have a confession to make. I've been cheating. All of the XAML that I've been using actually includes a UserControl and a Canvas:

<UserControl x:Class="LayoutPt2.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml>
    <Canvas>
        <Border BorderBrush="Blue" BorderThickness="12,4" CornerRadius="12,4,12,4" Background="LightBlue" Padding="4">
            <TextBlock Text="Fancy Border" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8"/>
        </Border>
    </Canvas>
</UserControl>

This is because if the Border was the child of the UserControl instead of the Canvas, it would take up the entire available space, which in this case would be the plugin space, because the default value for the HorizontalAlignment and VerticalAlignment are Stretch, which will cause the element to take up all of the available space. Often, this is exactly what you want, but for this example, I wanted to show what the Border would look like if it was not doing that. Here's what things would looks like without the Canvas:

<UserControl x:Class="LayoutPt1.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml>
    <Border BorderBrush="Blue" BorderThickness="12,4" CornerRadius="12,4,12,4" Background="LightBlue" Padding="4">
        <TextBlock Text="Fancy Border" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8"/>
    </Border>
</UserControl>

image

This fill behavior is typical, and is the result of an element being given more space than it actually needs. Unless the width and height have been set or constrained (e.g. by the MaxWidth and/or MaxHeight properties) or the HorizontalAlignment and/or VerticalAlignments set to a value other then Stretch, most elements will expand to fill the space assigned to them. The Margin and Padding properties have an effect on the minimum size only.

If an element in a layout container requires more space than is given to it, it is automatically clipped. Here's some XAML that specified the width of the Border, to make it narrower than the text requires:

<Canvas>
    <Border BorderBrush="Blue" Width="100" BorderThickness="12,4" CornerRadius="12,4,12,4" Background="LightBlue" Padding="4">
        <TextBlock Text="Fancy Border" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8"/>
    </Border>
</Canvas>

And here's how that looks:

image

You can see that the text does not go all the way up to the edge. That is because of the Margin and Padding. Those properties are applied before the content is clipped.

StackPanel

The StackPanel will place its children in either a column (default) or row. This is controlled by the Orientation property. A vertical StackPanel can have its width and height specified; if the width is not specified, it will be as wide as its widest child (or it will stretch to fit its parent), if the height is not specified it will take up as much space vertically as is required to fit all of its children. An unconstrained horizontal StackPanel will be as wide as necessary to hold all of its children, and as tall as the tallest child. Here is some XAML for a vertical StackPanel with its Width specified:

<StackPanel Background="Aquamarine" HorizontalAlignment="Center" VerticalAlignment="Center" Width="150">
    <Button Width="100" Content="Width=100" Margin="2"/>
    <Button Width="auto" Content="Width=auto" Margin="2"/>
    <Button Width="200" Content="Width=200" Margin="2"/>
</StackPanel>

This looks like:

image

The StackPanel has its Width set to 150. The first Button, which has its Width set to 100, is centered. The second button, which has its width set to "auto" (which is the same as not setting it) expands to fill the width of the StackPanel. The third Button, which has a Width of 200, is clipped, and is not centered. Note that all Buttons, whether they fit, were stretched to fill the StackPanel, or clipped, have their Margin property applied. If the alignment properties of the StackPanel are set to "Stretch", and the width is unconstrained, what happens? Here the XAML:

<UserControl x:Class="LayoutPt1.Page"
   
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Loaded="UserControl_Loaded">
    <
StackPanel Background="Aquamarine"HorizontalAlignment="Stretch"VerticalAlignment="Stretch">
        <
ButtonWidth="100"Content="Width=100"Margin="2"/>
        <
ButtonWidth="auto"Content="Width=auto"Margin="2"/>
        <
ButtonWidth="200"Content="Width=200"Margin="2"/>
    </
StackPanel>
</
UserControl>

Here's the result:

image

The StackPanel fills up the entire plugin space. The Buttons with their widths specified are centered; the Button with its Width set to "auto" is stretched fill the entire width. The bottom of the StackPanel is empty. There is no way to get the items in a vertical StackPanel to build from the bottom of the StackPanel; likewise a horizontal StackPanel's items will always be on the left. The VerticalAlignment property of a vertical StackPanel and the HorizontalAlighnment of a horizontal StackPanel are ignored. However, the HorizontalAlignment of an element in a vertical StackPanel, and the VerticalAlignment of an element in a horizontal StackPanel, will be honored. Here an example:

<StackPanel Background="Aquamarine" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <Button Width="100" Content="Width=100" Margin="2" HorizontalAlignment="Left"/>
    <Button Width="auto" Content="Width=auto" Margin="2"/>
    <Button Width="200" Content="Width=200" Margin="2" HorizontalAlignment="Right"/>
</StackPanel>

Here's what that looks like:

image

To emphasize the point about nested layout containers, here are some horizontal StackPanels inside of a vertical StackPanel:

<StackPanel Background="Gray" HorizontalAlignment="Center" VerticalAlignment="Center">
    <StackPanel Orientation="Horizontal" Background="Red" Margin="2">
        <Button Margin="2" Width="30" Content="A"/>
        <Button Margin="2" Width="30" Content="B"/>
        <Button Margin="2" Width="30" Content="C"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal" Background="White" Margin="2">
        <Button Margin="2" Width="30" Content="D"/>
        <Button Margin="2" Width="30" Content="E"/>
        <Button Margin="2" Width="30" Content="F"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal" Background="Blue" Margin="2">
        <Button Margin="2" Width="30" Content="G"/>
        <Button Margin="2" Width="30" Content="H"/>
        <Button Margin="2" Width="30" Content="I"/>
    </StackPanel>
</StackPanel>

This looks like:

image

Grid

The Grid is the most powerful layout container provided in Silverlight 2. It can have multiple children, and acts rather like a spreadsheet. The cells are not explicitly defined; you specify the rows and columns, and those define the cells. A row is the same height and a column is the same width across the entire Grid, but elements can be made to span multiple cells. Cells can contain more than one item.

The placement of an element in the Grid is specified using attached DependencyProperties that are set on the children of the Grid: Grid.Row, Grid.Column, Grid.RowSpan and Grid.ColumnSpan.

The size of rows and columns may be specified exactly, set to "auto", or use "star-sizing". If a row or column is set to "auto", it will be just as tall or wide as its tallest or widest child. Star-sizing is very powerful but a bit more complicated, so it will be discussed below.

Let's just launch right into it, and bring back the example from the first post in this series. I've modified it a little bit to make it more interesting.

<UserControl x