Welcome to MSDN Blogs Sign in | Join | Help

Customizing a ToolTip

(Special thanks to Andre Michaud and Mike Russell who showed me how to do this.)

ToolTips are quite useful for for displaying helpful information when the user hovers over a control. The way that ToolTips are usually used is by setting the ToolTipService.ToolTip property to some text. This will display the text in a rather bland rectangle, without wrapping. Here are some ways to make your ToolTips more useful and nicer looking.

ToolTip are typically defined like this:

<TextBox Height="20" Width="100" ToolTipService.ToolTip="Some helpful text"/>

The standard ToolTip looks like this:

image

ToolTipService.ToolTip is an attached property that defines the Content of the ToolTip. It will get put into a ContentPresenter. In this case, since it is a string, the ContentPresenter will create a TextBlock for it. The TextBlock does not wrap, however, so if you have too much text, it just looks silly. But knowing that the ToolTipService.ToolTip defines the Content of the ToolTip, you can add your own UIElements, and not rely on the ContentPresenter to do it for you.

Here's some XAML that makes a ToolTip that can wrap text. It also modifies some of the font properties:

<TextBox Height="20" Width="100">
    <ToolTipService.ToolTip>
        <TextBlock MaxWidth="150" 
                   Text="This is a longer string of text. It is even in a different font. Aren't ToolTips exciting?" 
                   FontFamily="Georgia" FontSize="14" TextWrapping="Wrap"/>
    </ToolTipService.ToolTip>
</TextBox>

This will show a ToolTip that is a little more interesting.

image

Because you can set the Content property to anything that you want, you can do this:

<TextBox Height="20" Width="100">
    <ToolTipService.ToolTip>
        <StackPanel>
            <Border Background="CadetBlue">
                <TextBlock Text="Some sort of title" TextAlignment="Center"/>
            </Border>
            <TextBlock MaxWidth="150" 
               Text="This is a longer string of text. It is even in a different font. Aren't ToolTips exciting?" 
               FontFamily="Georgia" FontSize="14" TextWrapping="Wrap"/>
        </StackPanel>
    </ToolTipService.ToolTip>
</TextBox>
This looks like this:

image

Note that the Content of the ToolTip can't be interacted with, so you can't put a Button (for example) in the ToolTip and expect the user to be able to interact with it, or for it to get the focus.

But this is still kind of a boring ToolTip: a plain-looking rectangle. But it is possible to make that look nicer, too.

You can retemplate the ToolTip to give it new visuals. Here's an example:

image

The XAML is below. You'll note that I rearranged things a little bit: I put a ToolTip element under the ToolTipService.ToolTip element. I made the ToolTip.Content just a simple TextBlock again, and set the ToolTip's Template property to a fancy new Template which is defined in the page resources.

<UserControl x:Class="ToolTipTest.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <UserControl.Resources>
        <ControlTemplate x:Key="ToolTipTemplate">
            <Border BorderBrush="Black" BorderThickness="4" CornerRadius="8" Background="PaleGoldenrod" MaxWidth="200">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <Grid Margin="2">
                        <Ellipse Fill="Black" Height="52" Width="52"/>
                        <Ellipse Stroke="White" StrokeThickness="4" Fill="Blue" Height="50" Width="50"/>
                        <TextBlock Text="i" FontStyle="italic" FontSize="40" FontFamily="Georgia" 
                                               VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="White"/>

                    </Grid>
                    <ContentPresenter Grid.Column="1"
                                        Content="{TemplateBinding Content}"
                                        ContentTemplate="{TemplateBinding ContentTemplate}"
                                        Margin="{TemplateBinding Padding}" 
                                        VerticalAlignment="Center"/>
                </Grid>
            </Border>
        </ControlTemplate>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="Gainsboro">
        <TextBox Height="20" Width="100">
            <ToolTipService.ToolTip>
                <ToolTip Template="{StaticResource ToolTipTemplate}">
                    <ToolTip.Content>
                        <TextBlock 
                           Text="This is a longer string of text." 
                           FontFamily="Georgia" FontSize="14" TextWrapping="Wrap"/>
                    </ToolTip.Content>
                </ToolTip>
            </ToolTipService.ToolTip>
        </TextBox>
    </Grid>
</UserControl>

Control Lifecycle

What happens when you create a Control? When do overrides get called and events get raised? When do styles get applied?

In response to this thread on silverlight.net, I've whipped this simple table up. There are some subtle differences between instantiating a control in XAML, and instantiating it via code that I've called out, but most of the lifecycle is the same.

Action Control instantiated in XAML Control instantiated in code
Control ctor As soon as begin tag is parsed. When you call it.
Explicit Style applied If the Style property is set in XAML, it will be applied as soon as the end tag is parsed. As soon as Style property is set.
Built-in Style (from generic.xaml) applied As soon as the end tag is parsed, after the explicit Style (if any) has been applied. Will not override explicit Style. When the control enters the tree. Will not override explicit Style.
Properties set When the attributes are parsed. When you set them.
Loaded event Posted when the element is been added to the tree. Fired before the next frame. Happens before layout. Same.
Template applied (i.e. control's visual are created from the Template) In the Measure pass of layout. The Template property will be applied if the control has no visual tree. The control starts life with no visual tree, and the visual tree will be cleared when the Template property is set. You can also call ApplyTemplate yourself. Same.
OnApplyTemplate called Whenever the Template is applied. It is not necessary to call the base OnApplyTemplate for the Template to be applied, but inherited types might be relying on it for their implementations. Same.
Visuals first available In OnApplyTemplate. Use GetTemplateChild. Same.
MeasureOverride called In the Measure pass of layout. If the Template was expanded during this Measure pass, MeasureOverride will be called after the Template has been expanded. Same.
ArrangeOverride called In the Arrange pass of layout, which occurs after the Measure pass. Same.
SizeChanged event After the Measure and Arrange passes have completed. Same.
LayoutUpdated event After SizeChanged events have fired. Same.

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:Class="LayoutPt1.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Loaded="UserControl_Loaded">
    <Grid Background="Beige">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Background="Coral">
            <Button Content="New" Width="50" Margin="2" HorizontalAlignment="Left"/>
            <Button Content="Open" Width="50" Margin="2"/>
        </StackPanel>

        <Grid Grid.Row="2" Grid.Column="1" Background="CornflowerBlue">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="Status text" VerticalAlignment="Center"/>
            <Button Grid.Column="1" Content="Help" Width="50" Margin="2"/>
        </Grid>

        <StackPanel Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Background="LightGreen">
            <Button Content="Command 1" Margin="2" HorizontalAlignment="Left"/>
            <Button Content="Command 2" Margin="2" HorizontalAlignment="Left"/>
            <Button Content="Command 3" Margin="2" HorizontalAlignment="Left"/>
        </StackPanel>
    </Grid>
</UserControl>

This looks like:

image

Let's examine the XAML. There are two Grids, and two StackPanels. The first Grid, the "outer" one, has a RowDefinition section like this:

<Grid.RowDefinitions>
    <RowDefinition Height="auto"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="auto"/>
</Grid.RowDefinitions>

This means that this Grid has three rows. These are numbered starting from 0, but in the definitions section, the number is implicit. The first definition is row 0, the next is row 1, etc. The only attribute a row has is its height. This is controlled by its Height, MinHeight and MaxHeight properties. When the Height is set to "auto", the row will only be as tall is it has to be to contain its children or to make an orderly Grid. The second Row has a size of "*". This means that the row will take up all left-over space. So if the Height of the Grid ends up being 100, and the first and last rows each take up 25, then the second row will be 50 high. It gets a little more complicated when more than one row or column is star-sized. The only item of note in the ColumnDefinition section is that the first column has a fixed size of 100.

The elements are placed in the Grid cells using the Grid.Row and Grid.Column properties.

Star-sizing in a Grid is a very powerful way of allocating relative amounts of space. The algorithm is pretty complicated (it has to deal with auto- and star-sized rows and columns, spanning, etc.) but the principle is simple: whatever space is left over from fixed- and auto-sized columns is allocated to all of the columns with star-sizing, according to the proportion of stars. Let's consider these ColumnDefinitions:

<Grid Background="Beige" Width="300">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

The Grid has a width of 300. Column 0 has a width of 100, and there is one star-sized column, so the star-sized column gets all of the space left over (200). If the definitions looked like this:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

Then columns 1 and 2 would gets widths of 100 each. But with these definitions:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>

the space is allocated differently. The total of the multipliers on the stars is 4 (the 1 on "*" is implicit.) So each star is now width 4 / 200 = 50. Column 1 will be 1 * 50 wide and column 2 will be 3 * 50 wide. Real numbers can also be used:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition Width=".5*"/>
    <ColumnDefinition Width="1.5*"/>
</Grid.ColumnDefinitions>

This ends up allocating the same amount of space in the previous example, because I chose nice numbers. The sum of the star multipliers is 2, so each star is worth 100 (in case you had forgotten the Grid is 300 wide, and the first column is 100 wide, so 200 will be divided up among the star-sized columns.) Column 1 is therefore .5 * 100 = 50 wide, and column 2 is 1.5 * 100 = 150 wide.

Column and row spanning are controlled by the Grid.ColumnSpan and Grid.RowSpan properties. When one of these is set on the child of a Grid, it will cover more than one column or row. In the example above, the StackPanel has a Grid.RowSpan of 3:

<StackPanel Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Background="LightGreen">
    <Button Content="Command 1" Margin="2" HorizontalAlignment="Left"/>
    <Button Content="Command 2" Margin="2" HorizontalAlignment="Left"/>
    <Button Content="Command 3" Margin="2" HorizontalAlignment="Left"/>
</StackPanel>

This causes the StackPanel to cover all three rows of the Grid, starting from the row specified by its Grid.Row property.

Silverlight Layout Fundamentals Part 1 - What is Layout?

This is the first part of a series of articles on Silverlight 2's layout system. I'm going to start off slowly, and demonstrate the need for a layout system, then explain some more about what "layout" actually is and what the Silverlight 2 layout system can do.

Silverlight 1.0 did not have layout functionality. Silverlight 2 does. But why is that a big deal? And what does "layout" mean, anyway? In generic terms, when you "lay something out" you are positioning things in a space. You could lay out a garden, a housing development, a grocery store, anything where you have some space to fill up, and things to fill it up with. This obviously includes placing visual elements on a computer screen.

In the first examples below, we'll ignore the fact that Silverlight 2 has a layout system, and do all of our layout the hard way.

Putting a button on the screen

Let's take the simple case of wanting to make a button by putting a TextBlock inside of a Rectangle in the top-left corner of the area occupied by the Silverlight plugin. (Let's ignore the fact that Silverlight 2 has a Button, so you wouldn't really need to make your own.)

Let's start with the Rectangle:

<UserControl x:Class="LayoutPt1.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml>
    <Rectangle Height="20" Width="100" Stroke="Black" Fill="Beige"/>
</UserControl>

So far, so good. (Note: I will not include the UserControl in subsequent examples, but it is still there.) Now, let's put the TextBlock in:

<Canvas>
    <Rectangle Height="20" Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock Text="Open"/>
</Canvas>

But this doesn't look so good. The text isn't in the right spot:

Rectangle and TextBlock #1

But how do you know where to put it? One way to do it would be use trial and error, and change the Canvas.Top and Canvas.Left properties of the TextBlock until it looks OK. But it seems like maybe this wonderful computer thingy could do that for you. Of course it can, but it will take some code.

<Canvas>
    <Rectangle x:Name="button" Height="20" Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText" Text="Open"/>
</Canvas>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;

namespace LayoutPt1
{
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
        }

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            Canvas.SetLeft(buttonText, (button.Width - buttonText.ActualWidth) / 2);
            Canvas.SetTop(buttonText, (button.Height - buttonText.ActualHeight) / 2);
        }
    }
}

(Note: In subsequent examples, I am only going to include the interesting bits of the code rather than the entire UserControl subclass file.) The text inside of the Rectangle looks better, but there is still something wrong. The whole thing is slammed up in the corner of the browser:

Rectangle and TextBlock #2

It would look nicer with some more room, so let's change the XAML.

<Canvas>
    <Rectangle x:Name="button" Canvas.Left="8" Canvas.Top="8" Height="20" 
               Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText" Text="Open"/>
</Canvas>

Oops. That doesn't look so good:

image

Looks like we should change the code to account for the fact the the Rectangle is no longer at 0,0.

Canvas.SetLeft(buttonText, Canvas.GetLeft(button) + (button.Width - buttonText.ActualWidth) / 2);
Canvas.SetTop(buttonText, Canvas.GetTop(button) + (button.Height - buttonText.ActualHeight) / 2);

Much better:

image

So now, we have some XAML and some code that will center a TextBlock inside of a Rectangle at 8,8.

Adding more buttons

Now that we have one button on the screen, let's add another. Where should we put it? Well, we forgot some white space around the button last time, so let's not forget it this time. The first button is at 8,8 and it is 100 wide and 20 high, and if we want a space of four pixels between the buttons for the margin, we should put the next button at 8 + 20 + 4 = 32. We should be able to copy, paste and modify our code to center the text.

<Canvas>
    <Rectangle x:Name="button1" Canvas.Left="8" Canvas.Top="8" Height="20" 
               Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText1" Text="Open"/>
    <Rectangle x:Name="button2" Canvas.Left="8" Canvas.Top="32" Height="20" 
               Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText2" Text="Save"/>
</Canvas>
Canvas.SetLeft(buttonText1, Canvas.GetLeft(button1) + (button1.Width - buttonText2.ActualWidth) / 2);
Canvas.SetTop(buttonText1, Canvas.GetTop(button1) + (button1.Height - buttonText2.ActualHeight) / 2);
Canvas.SetLeft(buttonText2, Canvas.GetLeft(button2) + (button2.Width - buttonText2.ActualWidth) / 2);
Canvas.SetTop(buttonText2, Canvas.GetTop(button2) + (button2.Height - buttonText2.ActualHeight) / 2);

Hah. Got it right the first try:

image

Now it gets complicated

But now, let's add some more buttons, which should be just like adding the "Save" button. But by the way, we want a "New" button to be on top, so we have to move everything down. And we also want our little stack of buttons in the bottom-right corner instead of the top left. We could do that by changing all of the Canvas.Left and Canvas.Top values. A little laborious, but not too bad. But what if we wanted to change the plugin size? Well, we could type in the numbers again. But what it the plugin was sized not absolutely, but as a percentage of the browser size, and we wanted our buttons to always be in the bottom right as the user resized the browser? Then we'd have to  do this:

<Canvas>
    <Rectangle x:Name="button0" Height="20" Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText0" Text="New"/>
    <Rectangle x:Name="button1" Height="20" Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText1" Text="Open"/>
    <Rectangle x:Name="button2" Height="20" Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText2" Text="Save"/>
    <Rectangle x:Name="button3" Height="20" Width="100" Stroke="Black" Fill="Beige"/>
    <TextBlock x:Name="buttonText3" Text="Save as..."/>
</Canvas>
void Content_Resized(object sender, EventArgs e)
{
    const double bigMargin = 8;
    const double smallMargin = 4;

    double right = Application.Current.Host.Content.ActualWidth - bigMargin;
    if (right < 0) return;

    double bottom = Application.Current.Host.Content.ActualHeight - bigMargin;

    PositionButton(button3, buttonText3, smallMargin, right, ref bottom);
    PositionButton(button2, buttonText2, smallMargin, right, ref bottom);
    PositionButton(button1, buttonText1, smallMargin, right, ref bottom);
    PositionButton(button0, buttonText0, smallMargin, right, ref bottom);
}

private void PositionButton(Rectangle r, TextBlock t, double margin, double right, ref double bottom)
{
    double left = right - r.Width;
    double top = bottom - r.Height;

    Canvas.SetLeft(r, left);
    Canvas.SetTop(r, top);
    Canvas.SetLeft(t, left + (r.Width - t.ActualWidth) / 2);
    Canvas.SetTop(t, top + (r.Height - t.ActualHeight) / 2);

    bottom -= r.Height + margin;
}

And it looks wonderful, and moves when the browser changes size:

image

But it was painful, and it was a lot of code to write just to move put four buttons where we wanted them. And even though things are nicely factored for putting a stack of buttons in the bottom right, imagine a screen full of buttons and such. And while you are designing, you'd add and remove elements, try different elements, move elements, etc. so you'd constantly be changing code to move things around. Yuck.

Layout to the rescue

So if you had never really thought about it before, it is easy to see why something that would do all of the dirty work for you would be nice.

That "something" in Silverlight 2 is the layout system. Silverlight 2 adds layout container elements. These layout container elements define how their children will be sized and positioned. A simple and very useful layout container is the Border element, which is similar to a Rectangle in many ways, but it has a single child that it centers inside of it by default.

This XAML will put our single "button" in the right spot in the upper left like we did above, but no code is necessary (obviously Silverlight is working away behind the scenes, but that's it):

<Canvas>
    <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="8">
        <TextBlock Text="New" HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Border>
</Canvas>

Another simple layout container is the StackPanel. By default, this puts its children in a vertical stack, just like we did. Here's what that XAML looks like, again without having to write any code:

<Canvas>
    <StackPanel Margin="6">
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="New" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="Open" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="Save" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="Save as..." HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
    </StackPanel>
</Canvas>

This looks exactly the same as our upper-left button stack. There are some more attributes in the XAML, but that's a small price to pay. The XAML for the bottom-right stack of buttons looks like this:

<Border>
    <StackPanel Margin="6" HorizontalAlignment="Right" VerticalAlignment="Bottom">
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="New" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="Open" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="Save" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        <Border BorderBrush="Black" BorderThickness="1" Background="Beige" Height="20" Width="100" Margin="2">
            <TextBlock Text="Save as..." HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
    </StackPanel>
</Border>

By replacing the Canvas (which doesn't really participate in layout) with a Border, and setting the horizontal and vertical alignments on the StackPanel, we now have a stack of buttons in the bottom-right of the plugin, that will automatically reposition itself if the plugin changes size. Did I mention that this example contains no code?

What does the Silverlight 2 layout system do?

The Silverlight 2 layout system has two main tasks:

  1. Figure out how big elements are
  2. Figure out where to put them

To do this, it relies on the fact that all elements that can have children know how to position their children. It introduces layout elements that have specific positioning behavior:

The Border has one child, that is centered by default.

The StackPanel can have any number of children, and arranges them in either a column (the default) or a row.

The Grid can have any number of children, and it arranges them sort of like a spreadsheet does. You can define rows and columns, and specify which rows and columns your elements can go into.

It is also possible to define your own layout containers, but that is an advanced topic.

Silverlight 2 also has a number of properties that layout uses, such as Margin, HorizontalAlignment, VerticalAlignment, MinWidth, MaxWidth, MinHeight, MaxHeight, etc.

The power of layout is demonstrated when you put layout containers inside of layout containers. For the next example, let's say that you want your Silverlight 2 app to look like a Windows app, with a row of buttons across the top, a line for status information on the bottom, and a row of buttons down the left side.

This could be done a number of ways, but when you start dividing the screen into rows and columns, a Grid is the natural way to think of things. I'll dive into the Grid more in subsequent posts. This is how your app would look (and I'm going to use real Buttons this time):

image

This is 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">
    <Grid Background="Beige">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        
        <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Background="Coral">
            <Button Content="New" Width="50" Margin="2" HorizontalAlignment="Left"/>
            <Button Content="Open" Width="50" Margin="2"/>
        </StackPanel>
     
        <Grid Grid.Row="2" Grid.Column="1" Background="CornflowerBlue">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="Status text" VerticalAlignment="Center"/>
            <Button Grid.Column="1" Content="Help" Width="50" Margin="2"/>
        </Grid>

        <StackPanel Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Background="LightGreen">
            <Button Content="Command 1" Margin="2" Width="80" HorizontalAlignment="Left"/>
            <Button Content="Command 2" Margin="2" Width="80" HorizontalAlignment="Left"/>
            <Button Content="Command 3" Margin="2" Width="80" HorizontalAlignment="Left"/>
        </StackPanel>

    </Grid>
</UserControl>

 

Posted by Dave Relyea | 3 Comments
Filed under: ,

Layout Transitions - An Animatable WrapPanel

 

I've been playing around with RenderTransforms and decorators and such to make layout transitions, but for this WrapPanel, my goal was to introduce no additional UI elements, animations or transforms. I also wanted to use some easing equations to do some cool transitions, and separate that logic from the WrapPanel. I wanted the elements the be "live" while they were on the screen, and I wanted them to enter the screen in a nice fashion. I also wanted each element to animate individually. One of my earlier versions of this kept track of a single starting time for all animations, and it wasn't cool enough. Sometimes, it is important to be cool.

WrapPanels

A WrapPanel is a layout container that places its elements in rows. It fills up one row, then moves on to the next one. The logic isn't all that tough. What I wanted to do was to make the items move smoothly when the WrapPanel was resized, and also enter the WrapPanel from the upper left and move to their new position.

Below are some of the interesting bits of the panel.

Using a zero-duration Storyboard to simulate a per-frame callback

Not using animations was one of my requirements, since I wanted to use something other than linear interpolation. The AnimateableWrapPanel uses a zero-duration Storyboard to simulate a per-frame callback. Here's how that is done. In the constructor, an empty Storyboard is created and started.

public AnimatedWrapPanel()

{

    // This sets up the per-frame callback that we use for animation.

 

    _tick = new Storyboard();

    _tick.Duration = new Duration(TimeSpan.Zero);

    _tick.Completed += Tick;

    _tick.Begin();

}

The Tick method does whatever I want to do each frame, and restarts the Storyboard.

/// <summary>

/// This tells the layout system to call Arrange on the Panel if there are any elements that

/// must be moved.

/// </summary>

private void Tick(object sender, EventArgs e)

{

    // If there are still elements to be animated, make sure that ArrangeOverride will

    // get called. Note that the InvalidateArrange may already have been called due to

    // other operations, but calling it again does not hurt, and all of the work will

    // be done in ArrangeOverride regardless of where the invalidation came from.

 

    if (_animatingElements > 0)

    {

        InvalidateArrange();

    }

 

    // Restart the storyboard so we get called back on the next frame.

    _tick.Begin();

}

This technique does not guarantee smooth animations. It is possible to overload the panel with too many elements for the framewrate to animate 100% smoothly, or maybe the rest of the app is doing so much work that the framerate slows down, but since I use the elapsed time to figure out where to put the elements, the animations will take the same amount of time on all machines. If there is too much going on in the panel and the app, on a low-end machine, the animation might be choppy, but will still finish in the specified time, or one frame longer if the timing is just off.

Using layout to "animate" the elements

I'm a little conflicted about this technique, but I have seen other people use it, so I thought I'd try it. It is a two stage process. The MeasureOverride call figures out where everything should go, and is the only place where the animations are started. This works because MeasureOverride will be called if the AnimatedWrapPanel changes size, or any of its children change size, or children are added or removed. So size changes, adding items etc. kicks off the animations. In ArrangeOverride, any element that is not where it is supposed to be is moved in that direction according to how much it should be done, and the interpolations. Here's what the ArrangeOverride looks like:

/// <summary>

/// A "normal" ArrangeOverride would just put things where they belong. What this one does

/// is to move the children towards their destinations according to the virtual animation

/// data that has been attached to each element. When they get there, the virtual animation

/// is turned off.

/// </summary>

protected override Size ArrangeOverride(Size finalSize)

{

    DateTime now = DateTime.Now;

 

    foreach (UIElement child in Children)

    {

        AnimatedWrapPanelAttachedData data = GetAnimatedWrapPanelAttachedData(child);

 

        TimeSpan elapsed = data.GetElapsed(now);

 

        if (elapsed < Duration || data.TargetPosition != data.CurrentPosition)

        {

            // The virtual animation is not done yet, so figure out how far along it is...

            double progress = (Duration.TimeSpan != TimeSpan.Zero) ? Math.Min(elapsed.TotalMilliseconds / Duration.TimeSpan.TotalMilliseconds, 1.0) : 1;

 

            // ...and what the next position is.

            Point newPosition = BlendPoint(_interpolation, data.StartPosition, data.TargetPosition, progress);

            child.Arrange(new Rect(newPosition.X, newPosition.Y, child.DesiredSize.Width, _rowHeights[data.Row]));

            data.CurrentPosition = newPosition;

        }

        else

        {

            // This element is not animating, but it might have become invalid on its own, so it still

            // needs to be arranged. The layout system will do as little as possible.

            child.Arrange(new Rect(data.CurrentPosition.X, data.CurrentPosition.Y, child.DesiredSize.Width, _rowHeights[data.Row]));

            if (data.IsAnimating)

            {

                --_animatingElements;

 

                // This is the only place where IsAnimating is set to false. This turns off the virtual animation.

                data.IsAnimating = false;

            }

        }

    }

 

    return finalSize;

}

Attached property bag

Rather than have a bunch of hash tables or something in the panel itself, I wanted to keep all of the state that I needed on the elements, which sounded like a job for attached DependencyProperties. However, I got tired of creating and modifying them as I changed my mind, and decided that it would be easier at design-time and more efficient at run-time if I had a single attached property that was actually a class that had a bunch of properties--an attached property bag. This would be efficient because I always used all of the properties for each element.

Interpolations using easing equations

Rather then hard-coding animations into the panel, or using a simple linear interpolation, I wanted to use pluggable interpolations. The interpolations take a double that indicates progress, where zero is "just starting" and one is "completed". They return a number (alpha) where zero refers to the start position, and one refers to the end position, although the number may be less than zero or greater than one. The general equation is:

alpha = fn(progress)

The linear interpolation is the simplest: it just gives returns the progress, i.e. alpha = progress.

In practice, the interpolations are used to blend the starting and target positions. Here's how the intermediate points are arrived at:

/// <summary>

/// Given an interpolation, the starting and ending positions and a number from 0-1 that represents how

/// far along the virtual animation is, calculate the new position.

/// </summary>

Point BlendPoint(Interpolation interpolation, Point from, Point to, double progress)

{

    Point p = new Point();

    double alpha = interpolation.GetAlpha(progress);

    p.X = from.X + alpha * (to.X - from.X);

    p.Y = from.Y + alpha * (to.Y - from.Y);

    return p;

}

I used some standard interpolations, but it is possibly and easy to define your own, and then just plug them into the panel. I set up the interpolations with some default values that I thought looked nice, but it is possible to get quite creative. 

Using the demo app

This should be pretty self-explanatory, but you can add pretty, randomly-colored Rectangles, text and image thumbnails. Clicking on a Rectangle deletes it from the panel. You can also change the size of all of the children, select the duration of the animations, and the interpolation that will be used. Resize the browser to watch the UI move around. I had wanted to have UI to modify the interpolation parameters, but at some point, you have to stop polishing and move one. Shipping is the art of stopping. So that bit of UI is left as an exercise for the developer.

Update:

Karen Corby has posted a live version of my sample app, and a version of her FlickrViewr that she modified to use the AnimatingWrapPanel, here. I love how the images come in as they are downloaded.

 

Using VS Snippets to Add DependencyProperties and Attached DependencyProperties

It can be a little tedious to type in everything that you need to define a DependencyProperty but you can use VS 2008's snippets to make is easy. The snippets are designed to work with WPF, but they can be made to work with Silverlight with one small change.

First, you need to insert the snippet. In the VS 2008 editor, right-click at the location in your class file where you want to define the DependencyProperty, and select "Insert Snippet" from the context menu.

When I do that, I get another menu with "NetFX30", "Other", and "C#" options. Select "NetFX30".

Now, you'll get another menu with "Define a DependencyProperty" and "Define an attached DependencyProperty". Choose whichever one you like.

You'll see something like the code below. You can tab between the highlighted text (the border indicates which one you are updating), and enter your own values.

public int MyProperty

{

    get { return (int)GetValue(MyPropertyProperty); }

    set { SetValue(MyPropertyProperty, value); }

}

 

// Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...

public static readonly DependencyProperty MyPropertyProperty =

    DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerclass), new UIPropertyMetadata(0));

When you change a value, VS will update the rest of the snippet. Once you have entered the property type, name, and ownerclass, you need to decide what to do with the UIPropertyMetadata. Silverlight does not support that class, so if you want to add a PropertyChangedCallback, you'll have to change the type from UIPropertyMetadata to PropertyMetadata, and pass the delegate to the ctor. This looks like this:

// Using a DependencyProperty as the backing store for Foo.  This enables animation, styling, binding, etc...

public static readonly DependencyProperty FooProperty =

    DependencyProperty.Register("Foo", typeof(double), typeof(CustomAttachedDP), new PropertyMetadata(OnFooChanged));

 

private void OnFooChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

}

 

If you don't want a PropertyChangedCallback, just pass null instead of the PropertyMetadata.

It is pretty easy to modify snippets and create your own. For example, you could make copies of the snippets above, change UIPropertyMetadata to PropertyMetadata, and add the property changed notification handler. Here is a link that will take you to all you ever wanted to know about snippets: http://msdn.microsoft.com/en-us/library/ms165392.aspx. Below is what my new snippet (added to the My Snippets folder) looks like, with changes and additions highlighted.

[Clarification: The example above is for a "regular" DependencyProperty; the modified snippet below is for an attached DependencyProperty. The principle is the same.]

<?xml version="1.0" encoding="utf-8" ?>

<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">

  <CodeSnippet Format="1.0.0">

    <Header>

      <Title>Define an attached DependencyProperty (Silverlight)</Title>

      <Shortcut>spropa</Shortcut>

      <Description>Code snippet for an attached property using DependencyProperty as the backing store</Description>

      <Author>Microsoft Corporation</Author>

      <SnippetTypes>

        <SnippetType>Expansion</SnippetType>

      </SnippetTypes>

    </Header>

    <Snippet>

      <Declarations>

        <Literal>

          <ID>type</ID>

          <ToolTip>Property Type</ToolTip>

          <Default>int</Default>

        </Literal>

        <Literal>

          <ID>property</ID>

          <ToolTip>Property Name</ToolTip>

          <Default>MyProperty</Default>

        </Literal>

        <Literal>

          <ID>ownerclass</ID>

          <ToolTip>The owning class of this Property.  Typically the class that it is declared in.</ToolTip>

          <Default>ownerclass</Default>

        </Literal>

        <Literal>

          <ID>defaultvalue</ID>

          <ToolTip>The default value for this property.</ToolTip>

          <Default>0</Default>

        </Literal>

      </Declarations>

      <Code Language="csharp">

        <![CDATA[

               

public static $type$ Get$property$(DependencyObject obj)

{

    return ($type$)obj.GetValue($property$Property);

}

 

public static void Set$property$(DependencyObject obj, $type$ value)

{

    obj.SetValue($property$Property, value);

}

 

// Using a DependencyProperty as the backing store for $property$.  This enables animation, styling, binding, etc...

public static readonly DependencyProperty $property$Property =

    DependencyProperty.RegisterAttached("$property$", typeof($type$), typeof($ownerclass$), new PropertyMetadata(On$property$Changed));

 

private void On$property$Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

 

}

$end$]]>

      </Code>

    </Snippet>

  </CodeSnippet>

</CodeSnippets>

 

 

Using an Attached DependencyProperty to Implement Pixel Snapping as an Attached Behavior

In a previous post, I introduced the Snapper element, which is a UserControl subclass that snaps its Content to an integer pixel. Now I'll show how to implement snapping as an attached behavior using a custom attached DependencyProperty.

To use the Snapper element, you put it into your tree, and "wrap" the element that you want to snap, like this:

<local:Snapper>
    <Rectangle Height="40" Width="40" Stroke="Black" Margin="2"/>
</local:Snapper> 

That's not bad, but there's a cooler way to do it. We can add pixel snapping to any element by using an attached property to attach a behavior, like this:

<Rectangle Height="40" Width="40" Stroke="Black" Margin="2" local:PixelSnapBehavior.PixelSnap="Closest"/>

 

Attached Properties 

Using XAML's attached properties, it is possible to put a property on an element that doesn't know about the property at compile time. In other words, "normal" properties have to be implemented on an element's class or base classes. Attached properties do not have this restriction. Some examples of attached properties include Canvas.Left, Canvas.Top, Canvas.ZIndex, Grid.Row, Grid.Column, etc. (If you have to put a "dot" in the property name, it is an attached property.) You can also create your own attached properties, and get and set their values on other objects. Before describing how to create your own attached properties, let's review regular DependencyProperties.

Dependency Properties

A "regular" DependencyProperty is a property that you define for a given class, and it is only valid on that class, like a CLR property. You register the DependencyProperty, and define a CLR property for it. A change notification callback is optional--you can pass null instead of the PropertyMetadata. Here are the elements needed to define a DependencyProperty. Note that the property and the change notification are static. Also note that the class you define the property on must descend from DependencyObject. In this case, I'm using a UserControl.

public class DPExample : UserControl

{

    static DependencyProperty DistanceProperty = DependencyProperty.Register("Distance", typeof(double), typeof(DPExample), new PropertyMetadata(OnDistanceChanged));

 

    static private void OnDistanceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)

    {

        Debug.WriteLine("Distance property changed from {0} to {1}", e.OldValue, e.NewValue);

    }

 

    public double Distance

    {

        get { return (double)GetValue(DistanceProperty); }

        set { SetValue(DistanceProperty, value); }

    }

}

You use the DPExample.Distance property like you would use any CLR property, but the underlying storage is provided by the Silverlight property system. You can set the property in code, in XAML, etc. and you will get change notification. You can also animate it if it is of a type that can be animated.

 

Attached DependencyProperties


An attached DependencyProperty is defined on one class, but made to by used (mostly) on instances of other classes. It is not necessary for the other classes to know about this property at compile time, so you can set an attached DependencyProperty on any object that descends from DependencyObject--even objects provided in the Silverlight framework. The property is now registered with RegisterAttached (the parameters are the same.) The property get has been replaced by a public static void Get<propertyName> method, and the property setter has been replaced by a public static <type> Set<propertyName> method.

 

static DependencyProperty MassProperty = DependencyProperty.RegisterAttached("Mass", typeof(double), typeof(DPExample), new PropertyMetadata(OnMassChanged));

 

static private void OnMassChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)

{

    Debug.WriteLine("Mass property changed from {0} to {1}", e.OldValue, e.NewValue);

}

 

public static void SetMass(DependencyObject obj, double value)

{

    obj.SetValue(MassProperty, value);

}

 

public static double GetMass(DependencyObject obj)

{

    object result = obj.GetValue(MassProperty);

 

    return result != null ? (double)result : DefaultMass;

}

 

public const double DefaultMass = 1;

You will notice that when the GetMass method calls GetValue, it does not immediately cast to a double. This is because if the property has not been set on the instance passed in by the obj parameter, GetValue will return null. In this case, it is typical to return a default value (as above) or some value that signals "not set". This attached DependencyProperty can be set in code by calling the SetMass method, set in XAML, and animated.

 

Attached Behaviors

 

If we define a behavior as "doing something" then when we attach a behavior to an element, we are getting it to do something that it could not do before. This is typically done by attaching an event handler or handlers to an elements events. The behavior is in the event handler, which is defined elsewhere. This is obviously easy enough to do in code, but it can also be done in XAML (and code) by leveraging attached DependencyProperties. When an attached DependencyProperty is set on an instance, the property changed notification method is called. This is where you can hook into the element's events.

 

The Code

 

Here is a class that implements pixel snapping as an attached behavior. 

 

using System;

using System.Collections.Generic;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Media;

 

using System.Diagnostics;

 

namespace CustomAttachedDP

{

    public class PixelSnapBehavior

    {

        // Define the attached DependencyProperty

        public static DependencyProperty PixelSnapProperty = DependencyProperty.RegisterAttached(

            "PixelSnap", typeof(PixelSnapType), typeof(PixelSnapBehavior), new PropertyMetadata(SnapPropertyChanged));

 

        // In the property changed notification method, we will add the element to a list of objects that will

        // be snapped when we get a LayoutUpdated event. We have to do a bunch of fancy stuff with weak references,

        // our own list, etc. because there is no Unloaded event.

        public static void SnapPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

        {

            PixelSnapType newSnap = (PixelSnapType)e.NewValue;

 

            int index = 0;

            while (index < _objects.Count)

            {

                if (_objects[index].Target == d)

                    break;

 

                ++index;

            }

 

            if (index < _objects.Count)

            {

                if (newSnap == PixelSnapType.None)

                {

                    if (_objects[index].IsAlive)

                    {

                        Debug.WriteLine("Removing RenderTransform");

                        ((FrameworkElement)_objects[index].Target).RenderTransform = null;

                    }

                    _objects.RemoveAt(index);

                }

            }

            else if (newSnap != PixelSnapType.None)

            {

                _objects.Add(new WeakReference(d));

            }

 

            if (!_attached && _objects.Count > 0)

            {

                FrameworkElement element = d as FrameworkElement;

                if (element != null)

                {

                    element.LayoutUpdated += new EventHandler(LayoutUpdated);

                    _attached = true;

                }

            }

            else if (_attached && _objects.Count == 0)

            {

                FrameworkElement element = d as FrameworkElement;

                if (element != null)

                {

                    element.LayoutUpdated -= new EventHandler(LayoutUpdated);

                    _attached = false;

                }

            }

        }

 

        // The attached DependencyProperty setter

        public static void SetPixelSnap(DependencyObject obj, PixelSnapType value)

        {

            obj.SetValue(PixelSnapProperty, value);

        }

 

        // The attached DependencyProperty getter

        public static PixelSnapType GetPixelSnap(DependencyObject obj)

        {

            object result = obj.GetValue(PixelSnapProperty);

 

            return result == null ? PixelSnapType.None : (PixelSnapType)result;

        }

 

        // A utility method to remove all snapped objects from the list.

        public static void RemoveAll()

        {

            while (_objects.Count > 0)

            {

                if (_objects[0].IsAlive)

                {

                    SetPixelSnap((DependencyObject)_objects[0].Target, PixelSnapType.None);

                }

                else

                {

                    _objects.RemoveAt(0);

                }

            }

        }

 

        // The event handler for the LayoutUpdated event. It will snap everything that it thinks is

        // still alive. This is not 100% bulletproof but it should work in most scenarios.

        private static void LayoutUpdated(object sender, EventArgs e)

        {

            int index = 0;

 

            while (index < _objects.Count)

            {

                if (_objects[index].IsAlive == false)

                {

                    _objects.RemoveAt(index);

                }

                else

                {

                    Snap(_objects[index].Target as FrameworkElement);

                    ++index;

                }

            }

        }

 

        // Try to align an element on an integer pixel

        private static void Snap(FrameworkElement target)

        {

            if (target == null)

                return;

 

            PixelSnapType snap = PixelSnapBehavior.GetPixelSnap(target);

 

            // Remove existing transform

 

            TranslateTransform savedTransform = target.RenderTransform as TranslateTransform;

            if (savedTransform != null)

            {

                target.RenderTransform = null;

            }

 

            // Calculate actual location

 

            MatrixTransform globalTransform = target.TransformToVisual(Application.Current.RootVisual) as MatrixTransform;

            Point p = globalTransform.Matrix.Transform(_zero);

 

            double deltaX = snap == PixelSnapType.Closest ? Math.Round(p.X) - p.X : (int)p.X - p.X;

            double deltaY = snap == PixelSnapType.Closest ? Math.Round(p.Y) - p.Y : (int)p.Y - p.Y;

 

            // Set new transform

 

            if (deltaX != 0 || deltaY != 0)

            {

                if (savedTransform == null)

                    savedTransform = new TranslateTransform();

 

                target.RenderTransform = savedTransform;

 

                savedTransform.X = deltaX;

                savedTransform.Y = deltaY;

            }

        }

 

        private static readonly Point _zero = new Point(0, 0);

        private static List<WeakReference> _objects = new List<WeakReference>();

        private static bool _attached = false;

    }

 

    public enum PixelSnapType

    {

        None,

        Closest,

        TopLeft

    }

}

Navigation with Animated Transition Effects

One of the common things that developers want to do is to navigate between pages of their application. Once they have that, then they want to make the transitions look pretty. The attached project (see below) show how to do use a TransitionControl to do both. The TransitionControl is extensible, so you can even write your own transitions. You can download the project by clicking on the Attachment links on the bottom of the post. Here is a snapshot of the demo app that shows the Image Page in mid-spin as it appears on top of the Controls Page:

How it works

The TransitionControl is subclassed from UserControl. This is its public API:

public partial class TransitionControl : UserControl

{

    public TransitionControl();

 

    public void GoTo(FrameworkElement newContent, ITransition transition, TimeSpan duration);

    public void Completed();

 

    public void CurrentToBack();

    public void CurrentToFront();

 

    public FrameworkElement New { get; }

    public FrameworkElement Current { get; }

}

 

The GoTo method is the only method that you'll call to display new content. You pass in the new content, the transition that you wish to use, and how long you want the transition to take. The other methods and properties are for use by the transition. The sample application uses a TransitionControl for its main, but not the entire page. There is nothing magic about it; it is just a control. You can use more than one, make them any size you like, fill up the page with it, etc.

ITransition

The ITransition interface is as follows:

public interface ITransition

{

    void GoTo(TransitionControl control, TimeSpan duration);

    void Stop();

}

The Goto method is called by the TransitionControl, which passes in itself and the duration it received in its GoTo method. The Stop method is called by the TransitionControl when the TransitionControl.GoTo method is called before the a transition already in progress can finish.

The simplest implementation of ITransition included in the project is Swap:

/// <summary>

/// This is the simplest transition class, and is about all you can really do

/// without animating. This is a synchronous transition.

/// </summary>

public class Swap : ITransition

{

    public void GoTo(TransitionControl control, TimeSpan duration)

    {

        control.CurrentToBack();

        control.Completed();

    }

 

    public void Stop()

    {

        // Nothing to do here, since this is not async

    }

}

The Swap transition ignores the duration and simply bring the new content to the front by calling control.CurrentToBack() and lets the control know that the transition is finished by calling control.Completed.

Animated transitions

Of course, merely swapping the content is not all that interesting. To get some nice effects, we'll need to animate. There is an abstract class in the sample that helps with this.

/// <summary>

/// The StoryboardTransition classs is designed to encapsulate an asynchronous,

/// animated transition.

/// </summary>

public abstract class StoryboardTransition : ITransition

{

    public void GoTo(TransitionControl control, TimeSpan duration)

    {

        _control = control;

        _storyboard = new Storyboard() { Duration = duration };

        Storyboard.Completed += storyboardCompleted;

        control.New.Resources.Add("storyboard", _storyboard);

        GoToCore(duration);

        Storyboard.Begin();

    }

 

    public void Stop()

    {

        _storyboard.Stop();

    }

 

    protected Storyboard Storyboard { get { return _storyboard; } }

    protected TransitionControl Control { get { return _control; } }

 

    // The subclass adds their animations to the Storyboard in this override.

    protected abstract void GoToCore(TimeSpan duration);

 

    protected void storyboardCompleted(object sender, EventArgs e)

    {

        CompletedCore();

        _control.Completed();

    }

 

    // The subclass can remove the stuff needed only for the transition,

    // such as clips or transforms.

    protected virtual void CompletedCore() {}

 

    private Storyboard _storyboard;

    private TransitionControl _control;

}


To implement a subclass of the StoryboardTransition, you'll need to override GoToCore and add your animations to the Storyboard provided. You may also need to add some things to the new content, such as RenderTransforms or Clips. By overriding the CompletedCore transition, you get the opportunity to remove whatever you added that is no longer needed.

The FadeIn transition

The FadeIn transition is a simple animated transition that gives you an idea of how to subclass StoryboardTransition:

public class FadeIn : StoryboardTransition

{

    protected override void GoToCore(TimeSpan duration)

    {

        Control.CurrentToBack();

 

        DoubleAnimation opacityAnimation = new DoubleAnimation() { From = 0, To = 1, Duration = duration, FillBehavior=FillBehavior.HoldEnd };

        Storyboard.SetTarget(opacityAnimation, Control.New);

        Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));

 

        Storyboard.Children.Add(opacityAnimation);

    }

} 

The GoToCore method is overridden (as it must be, since it abstract). The first thing it does is send the current content to the back, since the new content will be appearing in front of it. They it sets up the opacity animation and adds it to the Storyboard. That's all there is to do, because there is nothing to clean up. Note that the FillBehavior has been set to HoldEnd, meaning that if the animation is stopped, it will jump to the To value and stay there.

Other transitions

Here are the transitions that the project supplies:

Swap
Wipe (you can specify the direction)
FadeIn
FadeOut
(I thought there would be a difference between fading in and out, but now I'm not so sure...)
Open (a circle opens up to reveal the new content)
Close (a circle closes, hiding the old content)
Grow (the new content zooms in from a point in the middle of the screen; spinning is optional)
Shrink (the old content disppears to a point in the middle of the screen; spinning is optional)

You can also write your own. If you make a cool transition, I'd love to see it.

DiscreteSlider - Adding Functionality with a Simple Control Subclass

How do you prevent the Slider from returning "ugly" values, and get it to "snap" to only the values that you want, such as integers, or multiples of a certain step value such as 0.125? One way to do this is to override OnValueChanged, but it helps to understand how that mechanism works. Below is an example of subclassing Slider to add that functionality. It overrides OnValueChanged to alter the behavior of Slider to return only multiples of the SmallChange value, and the Thumb will snap to those values.

When the Value property changes, whether by the user dragging the Thumb or programmatically, the OnValueChanged virtual method is called. The Slider's implementation of OnValueChanged will raise the ValueChanged event, which is how most apps respond to the user moving the Slider's Thumb. The OnValueChanged method has the old and new values in its parameters. To get the default Slider behavior, if you override OnValueChanged you pass those two parameters back to the base implementation of OnValueChanged. But what if you don't call base.OnValueChanged, and what if you change the values that you pass back?

For starters, if you don't call the base implementation, the ValueChanged event will not get fired. The Value property will change, but there will no event raised. It turns out that passing different values in base.OnValueChanged is only partially useful--if you pass different values, those are the values are used to make the RoutedPropertyChangedEventArgs, but it has no effect on the Value property. So if you want to modify the Value property, you'll have to set it yourself, but doing so inside of the ValueChanged override will cause reentrancy

By overriding the OnValueChanged, changing the values that you pass back to base.OnValueChanged, and changing the Value property (provided you take care of reentrancy), you can determine when the ValueChanged event is raised, and the values that the Value property will take.

Let's take a look at some of the code. This snippet does the work of converting the new value to a multiple of SmallChange:

double newDiscreteValue = (int)(Math.Round(newValue / SmallChange)) * SmallChange;

After we've done that, we check to see if the new discrete value is different from our old one. In other words, the Slider may think that the Value has changed from 4.0 to 4.2, but if our SmallChange value is .5, then we want to stay at 4.0. We set the new Value (this is what causes the reentrancy), call base.OnValueChanged, then save the discrete value for next time:

if (newDiscreteValue != m_discreteValue)

{

    Value = newDiscreteValue;

    base.OnValueChanged(m_discreteValue, newDiscreteValue);

    m_discreteValue = newDiscreteValue;

}

Notice that if the discrete value did not change, we do not call base, so the event is not fired, and the Thumb does not move.

When we set the Value property, the OnValueMethod will get called again while we're still in it, so we mitigate against this by using the m_busy flag.

Here's the code:

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

namespace TransitionApp

{

    public class DiscreteSlider : Slider

    {

        protected override void OnValueChanged(double oldValue, double newValue)

        {

            if (!m_busy)

            {

                m_busy = true;

 

                if (SmallChange != 0)

                {

                    double newDiscreteValue = (int)(Math.Round(newValue / SmallChange)) * SmallChange;

 

                    if (newDiscreteValue != m_discreteValue)

                    {

                        Value = newDiscreteValue;

                        base.OnValueChanged(m_discreteValue, newDiscreteValue);

                        m_discreteValue = newDiscreteValue;

                    }

                }

                else

                {

                    base.OnValueChanged(oldValue, newValue);

                }

 

                m_busy = false;

            }

        }

 

        bool m_busy;

        double m_discreteValue;

    }

}

Posted by Dave Relyea | 2 Comments

Using Popup to create a Dialog class

Presenting a dialog to the user is a common task for many applications. Silverlight 2 does not have a Dialog class, but it is possible to use the Popup class to create a very credible dialog. My Dialog class can't leave the Silverlight plugin--for that you would have to use Javascript interop to create a new Silverlight plugin, etc. and that's a topic for somebody else's blog :)

The key here is to make the Popup full screen (and resize it when the plugin size changes) and also have a Canvas that is full screen. Then, if you want to have a modal dialog, set the Canvas's Background property. This will prevent mouse interaction with the rest of your application. This will also enable you to detect when the user clicks outside of your dialog content, in case you want to dismiss the dialog that way.  

The one caveat that I can think of is that keyboard events can still follow the focus, which can be set to elements beneath the modal dialog. I have some ideas how to fix this in the RTM timeframe.

To use the Dialog class, you will need to subclass it, and provide an override for the GetContent method. The content that you provide will become centered (in a Grid) on the screen.

Here is the code for the Dialog class:

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Controls.Primitives;

using System.Windows.Media;

using System.Windows.Shapes;

 

namespace DialogApp

{

    public abstract class Dialog

    {

        public void Show(DialogStyle style)

        {

            if (_isShowing)

                throw new InvalidOperationException();

 

            _isShowing = true;

 

            EnsurePopup(style);

 

            _popup.IsOpen = true;

 

            Application.Current.Host.Content.Resized += OnPluginSizeChanged;

        }

 

        public void Close()

        {

            _isShowing = false;

 

            if (_popup != null)

            {

                _popup.IsOpen = false;

                Application.Current.Host.Content.Resized -= OnPluginSizeChanged;

            }

        }

 

        // Override this method to add your content to the dialog

        protected abstract FrameworkElement GetContent();

 

        // Override this method if you want to do something (e.g. call Close) when you click

        // outside of the content

        protected virtual void OnClickOutside() { }

 

        // A Grid is the child of the Popup. If it is modal, it will contain a Canvas, which

        // will be sized to fill the plugin and prevent mouse interaction with the elements

        // outside of the popup. (Keyboard interaction is still possible, but hopefully when

        // Silverlight 2 RTMs, you can disable the root to take care of that.) The Grid isn't

        // strictly needed if there is always a Canvas, but it is handy for centering the content.

        //

        // The other child of the Grid is the content of the popup. This is obtained from the

        // GetContent method.

 

        private void EnsurePopup(DialogStyle style)

        {

            if (_popup != null)

                return;

 

            _popup = new Popup();

            _grid = new Grid();

            _popup.Child = _grid;

 

            if (style != DialogStyle.NonModal)

            {

                // If Canvas.Background != null, you cannot click through it

 

                _canvas = new Canvas();

                _canvas.MouseLeftButtonDown += (sender, args) => { OnClickOutside(); };

 

                if (style == DialogStyle.Modal)

                {

                    _canvas.Background = new SolidColorBrush(Colors.Transparent);

                }

                else if (style == DialogStyle.ModalDimmed)

                {

                    _canvas.Background = new SolidColorBrush(Color.FromArgb(0x20, 0x80, 0x80, 0x80));

                }

 

                _grid.Children.Add(_canvas);

            }

 

            _grid.Children.Add(_content = GetContent());

 

            UpdateSize();

        }

 

        private void OnPluginSizeChanged(object sender, EventArgs e)

        {

            UpdateSize();

        }

 

        private void UpdateSize()

        {

            _grid.Width  = Application.Current.Host.Content.ActualWidth;

            _grid.Height = Application.Current.Host.Content.ActualHeight;

 

            if (_canvas != null)

            {

                _canvas.Width = _grid.Width;

                _canvas.Height = _grid.Height;

            }

        }

 

        private bool _isShowing;

        private Popup _popup;

        private Grid _grid;

        private Canvas _canvas;

        private FrameworkElement _content;

    }

 

    public enum DialogStyle

    {

        NonModal,

        Modal,

        ModalDimmed

    };

}

 

Here is how I use it in my sample:

<UserControl x:Class="DialogApp.Page"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:DialogApp;assembly=DialogApp">

    <Border x:Name="LayoutRoot" Background="White">

        <StackPanel Margin="8" Width="120" VerticalAlignment="Top" HorizontalAlignment="Left">

            <Button Height="24" Margin="2" Content="Show Popup" Click="Button_Click"/>

            <RadioButton Height="24" Margin="2" Content="Non-Modal" IsChecked="true"/>

            <RadioButton Height="24" Margin="2" x:Name="isModal" Content="Modal"/>

            <RadioButton Height="24" Margin="2" x:Name="isModalDimmed" Content="Modal Dimmed"/>

        </StackPanel>

    </Border>

</UserControl>

 

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Media;

 

namespace DialogApp

{

    public partial class Page : UserControl

    {

        public Page()

        {

            InitializeComponent();

        }

 

        private void Button_Click(object sender, RoutedEventArgs e)

        {

            Dialog dlg = new MyDialog();

            if (isModal.IsChecked.Value)

                dlg.Show(DialogStyle.Modal);

            else if (isModalDimmed.IsChecked.Value)

                dlg.Show(DialogStyle.ModalDimmed);

            else

                dlg.Show(DialogStyle.NonModal);

        }

    }

 

    public class MyDialog : Dialog

    {

        protected override FrameworkElement GetContent()

        {

            // You could just use XamlReader to do everything except the event hookup.

 

            Grid grid = new Grid() { Width = 200, Height = 200, };

            Border border = new Border() { BorderBrush = new SolidColorBrush(Colors.Black), BorderThickness = new Thickness(2), CornerRadius = new CornerRadius(4), Background = new SolidColorBrush(Colors.White) };

            grid.Children.Add(border);

            grid.Children.Add(new TextBlock() { Text = "Dialog", HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(8) });

            Button button = new Button() { Width = 50, Height = 24, Content = "Close", HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, Margin = new Thickness(8) };

            grid.Children.Add(button);

            button.Click += (sender, args) => { Close(); };

 

            return grid;

        }

 

        protected override void OnClickOutside()

        {

            //Close();

        }

    }

}

 

 

 

 

Posted by Dave Relyea | 14 Comments
Filed under: , ,

Attachment(s): DevDaveDialogApp.zip

Layout Events - SizeChanged and LayoutUpdated

Executive Summary: Most of the time, SizeChanged is the right event to use, and LayoutUpdated is the wrong event. 

The Silverlight 2 layout system offers two events: SizeChanged and LayoutUpdated. They look the same...here is how they are hooked up in C#:

public Page()

{

    InitializeComponent();

 

    LayoutRoot.SizeChanged += LayoutRoot_SizeChanged;

    LayoutRoot.LayoutUpdated += LayoutRoot_LayoutUpdated;

}

The handlers are a bit different, though. Note that the LayoutUpdated event just has a standard EventArgs, while the SizeChanged event has the SizeChangedEventArgs, which very helpfully contains the old and new sizes.

void LayoutRoot_LayoutUpdated(object sender, EventArgs e)

{

}

 

private void LayoutRoot_SizeChanged(object sender, SizeChangedEventArgs e)

{

}

So far, they look pretty much the same, except for the additional information provided by the SizeChanged event. But this is not the case.

SizeChanged

The SizeChanged event is an "instance event" that is raised whenever the size of the element you have attached the handler to has changed. Note that if the position of the element on the screen changes, but not its size, this event does not get raised.

LayoutUpdated

The LayoutUpdated event is a "static event" that is fired every time layout had anything to do anywhere in the tree. You may find that it is getting fired much more often than you thought it would be. That's the reason why.

Sequence of events

The two events are also raised at different times. Here is roughly the way that the layout loop works:

while any element needs to be measured or arranged
{
    while any element needs to be measured
    {
        measure everything that needs measuring
    }

    while anything element needs to be arranged and no element needs to be measured
    {
        arrange everything that needs to be arranged
    }

    if any element needs to be measured or arranged
    {
        continue
    }

    if there are any SizeChanged events to raise
    {
        raise the SizeChanged events
    }

    if any element needs to be measured or arranged
    {
        continue
    }

    if layout did anything
    {
        raise the LayoutUpdated event
    }
}

Layout keeps looping until there was nothing for it to do or until the cycle detection kicks in. This means that you can change properties that affect layout in the SizeChanged and LayoutUpdated event handlers. In fact, it is kind of assumed that you'll be doing that in your SizeChanged handlers. The LayoutUpdated event fires when the layout system thinks it is all finished. I used it for my Snapper element only because I needed to be notified when the Snapper moved, not just when it changed size, and there is no other way to get that information.

Example

Here's an example of an app the handles the SizeChanged and LayoutUpdated events.

Don't forget to modify the x:Class to match your app.

<UserControl x:Class="LayoutEvents.Page"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Grid x:Name="LayoutRoot" Background="BurlyWood">

        <Grid.RowDefinitions>

            <RowDefinition Height="*"/>

            <RowDefinition Height="Auto"/>

        </Grid.RowDefinitions>

        <Rectangle Margin="4" Fill="AliceBlue"/>

        <StackPanel x:Name="buttonPanel" Width="150" Margin="8"  MinWidth="100" MaxWidth="200">

            <Button Content="Width += 10" Margin="2" Click="ButtonAdd_Click"/>

            <