This is the third (if you count Sunday’s update!) in my short series on Templates and Data Validation. [ First In Series. Previous In Series ] Today we’re going to create a templated Button that changes the appearance and visual state behavior of the button. Changing the visual state behavior in this case means responding to:
- MouseOver, by swelling
- MouseDown, by twisting and shrinking
- Disable, by fading
- Lost Focus and Get Focus (no change for this demo)
To get started, open a copy of the previous project in Blend, and increase the size` to 280, and change the relative height of the bottom row to 2* (two-star) to allow for a larger (round) button. Find the OK button in the Objects and Timeline pane (typically in the lower left) and right click on it, selecting Templates –> Create New and name the new template RoundButton saving it in the Styles.xaml resource file we created in the previous iteration. Blend will switch to the Template Editor, which is just what we want.
Creating the New Button’s Normal Appearance
Because we chose “Create New” rather than working from a copy of the existing Button, we currently have no appearance for the button at all. The new template consists of an empty grid, and if you assign this template to a button it will disappear! We begin, therefore, by creating the new button’s appearance within that grid. In this case, I’m going to give up on attempting to be artistic, and create a fairly simple round button. My strategy, stolen from someone else, way back in the mists of time, is as follows:
- Draw an ellipse that fits in the grid and has equal height and width (a circle)
- Add a radial gradient
- Add a tiny ellipse in the upper left to be the “light source” giving a slightly 3d look
- Add a TextBlock to hold the Button’s text
All of this can be done by dragging from the toolbox onto the design surface and then setting properties within Blend. Here is the resulting Xaml:
<ControlTemplate x:Key = "RoundButton" TargetType = "Button" > <Grid Height = "100" Width = "100" > <Ellipse x:Name = "ellipse" Width = "44" Height = "44" HorizontalAlignment = "Center" VerticalAlignment = "Center" RenderTransformOrigin = "0.5,0.5" StrokeThickness = "3" Stroke = "Blue" > <Ellipse.Fill> <RadialGradientBrush> <GradientStop Color = "#FF6F6FD2" Offset = "0.23" /> <GradientStop Color = "#FF0B1587" Offset = "0.9" /> <GradientStop Color = "#FF2C37AF" Offset = "0.5" /> </RadialGradientBrush> </Ellipse.Fill> <Ellipse.RenderTransform> <CompositeTransform /> </Ellipse.RenderTransform> </Ellipse> <Ellipse x:Name = "lightSpot" HorizontalAlignment = "Left" Height = "6" Margin = "39,39,0,0" Stroke = "Black" VerticalAlignment = "Top" Width = "6" Fill = "White" RenderTransformOrigin = "0.5,0.5" > <Ellipse.RenderTransform> <CompositeTransform /> </Ellipse.RenderTransform> </Ellipse> <TextBlock x:Name = "textBlock" HorizontalAlignment = "Center" VerticalAlignment = "Center" Height = "Auto" Width = "Auto" Margin = "15,20,15,15" Text = "OK" Foreground = "#FFFFFBFB" FontFamily = "Georgia" FontSize = "10" RenderTransformOrigin = "0.5,0.5" > <TextBlock.RenderTransform> <CompositeTransform /> </TextBlock.RenderTransform> </TextBlock> </Grid> </ControlTemplate>
Template With No View State
Before we go on, save this template and exit template editing mode by clicking on MainPage.xaml. Next, drag the template onto the Button from the Resources tab just as you previously dragged a style onto the button. The button changes its appearance. Run the program and you have a round button but hovering over it or clicking on it makes no change in its appearance. It is inert. To restore the expected behavior on mouse over, etc., we need to provide transitions to changes in the appearance for each view state. Return to the Resource editor by clicking on the Edit Resource button in the Resources pane as shown in the illustration. To have our new templated button perform as expected, we’ll update the common view states.
The easiest way to explain View States is to list a few: Mouse Over, Disabled, Pressed. Open any Silverlight application and look at a button. You can immediately see that its UI (the view) changes subtly as it enters and exits each of these states. You create this behavior for your own templates with the following steps that we’ll walk through here:
- Define the View States and the View State Groups (see below)
- Associate a storyboard with each View State that transitions from the previous appearance to the new appearance
- Designate how long it takes to transition from one state to another
View State Groups
A control can be in more than one view state at a time. Your button could, for example, be in the Enabled (vs. disabled) state and also in the MouseOver state. To avoid the potential combinatorial explosion of states, the Visual State Manager supports State groups. The rule is that it must not be possible to be in more than one state within any group – that is a group consists of mutually exclusive states. Every control should support the so-called “common states” and the focus states… Common States:
When we asked for a new template we did not get a blank slate; our new template was born with these two state groups in place (though they have no changes to the visual representation associated with them). What we want to do is to provide each of these states with the new visualization and information on how it should transition from one state to another. That information can be as simple as saying “whatever you looked like before going to this state, transition to the new state over this much time” or it can be as complex as creating a storyboard to animate the transition from one state to another, changing various properties at various rates. I will list the Xaml that is used to store these state transitions, but I didn’t write the Xaml directly; instead I clicked on each state and then used the animation timeline in Blend to record (and preview) the transitions I wanted, and Blend created the Xaml for me.
|If there were no other reason for programmers to adopt Blend into their programming toolbox, creating animations and visual state transitions would be more than enough.|
Creating New View State Behavior
Let’s start with something fun. When the user hovers the mouse over our new templated button, we’ll have the button swell to 140% of its original size. When the mouse moves away, we’ll have the button return to normal. This takes some pretty complex Xaml, but is a snap to do in Blend. Storyboards The secret sauce is Storyboards. Storyboards are the basis of animation in Silverlight. They consist of a number of animation statements. Each animation statement modifies one property on one control, changing it from a starting state to an ending state over a period of time. Fortunately Blend makes this easy to create.
Creating The Storyboard In Blend
In Blend, click on or open the States tab. This will cause a red box to surround the design surface with the words MouseOver state recording is on (1 in the diagram) in the upper left corner. [click the image to see it full size] Open the timeline, and click on Windows –> Workspaces Animation to get the window arrangement most convenient for recording storyboards, with the design surface on the top and the tabs across the bottom (affording room to spread out the timeline). To create a transition associated with the MouseOver expose the States pane (2) and click on the MouseOver state (3) The indicator turns red in the upper left (1) as noted above. Click on the parts of the control you are going to modify (4) and then click in the time line to establish an amount of elapsed time (6). Next, set the various properties to their new values. Click on the keyframe button (5) to record the modified state. You can “play” the animation by clicking the play button (7) You can, and often will, choose more than one spot on the timeline. In that case, you’ll set properties, click the keyframe button, move the timeline, set properties and click the keyframe button. Silverlight will interpolate a smooth transition between each keyframe time.
|NB: There is an important distinction between keyframe animation and from/to animation that I’m simply ignoring for now. More on that in a future tutorial.|
Animation and Xaml
Each time you click the keyframe button, a set of animation statements is written in Xaml as a resource. You could skip Blend altogether and write the Xaml yourself (you can also skip going to Staples, and make your own paper from tree bark) Here’s the Xaml for the MouseOver storyboard for you propeller heads. We’ll examine it in detail below…
<VisualState x:Name = "MouseOver" > <Storyboard> <DoubleAnimation Duration = "0:0:0.4" To = "1.4" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName = "body" d:IsOptimized = "True" /> <DoubleAnimation Duration = "0:0:0.4" To = "1.4" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName = "lightSpot" d:IsOptimized = "True" /> <DoubleAnimation Duration = "0:0:0.4" To = "1.4" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName = "text" d:IsOptimized = "True" /> <DoubleAnimation Duration = "0:0:0.4" To = "1.4" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName = "body" d:IsOptimized = "True" /> <DoubleAnimation Duration = "0:0:0.4" To = "1.4" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName = "lightSpot" d:IsOptimized = "True" /> <DoubleAnimation Duration = "0:0:0.4" To = "1.4" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName = "text" d:IsOptimized = "True" /> <PointAnimation Duration = "0:0:0.4" To = "0.278,0.611" Storyboard.TargetProperty = "(UIElement.RenderTransformOrigin)" Storyboard.TargetName = "lightSpot" d:IsOptimized = "True" /> <DoubleAnimation Duration = "0:0:0.4" To = "-3.166" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName = "lightSpot" d:IsOptimized = "True" /> <DoubleAnimation Duration = "0:0:0.4" To = "-1.167" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName = "lightSpot" d:IsOptimized = "True" /> </Storyboard> </VisualState>
Each animation statement acts on a property of a named UI object. For example, examine the first animation in the storyboard shown above, and reproduced here:
<DoubleAnimation Duration = "0:0:0.4" To = "1.4" Storyboard.TargetProperty = "(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName = "body" d:IsOptimized = "True" />
Let’s review the attributes in logical order:
- The Storyboard.TargetName attribute indicates which object we’ll be animating. Our RoundButton consists of three possible targets:
TargetName indicates that this animation will animate the body. The body consists of a number of properties such as height, width, foreground (color), etc. The Storyboard.TargetProperty tells you which property in the Target is being animated. It turns out that in this case what is being animated is the size of the object on the x axis; but it is being done through a mechanism more complex than we have to worry about here (!)
- The To attribute tells you the ending value for the TargetProperty – in this case the size on the x axis will be scaled to 1.4. This is a relative value indicating 1.4 times the starting size. To is often accompanied by From, but in this case we’ll scale up to 140% of whatever the current value is.
- The Duration attribute indicates how long the animation should take; in this case 4/10 of a second. Silverlight will interpolate all the intermediate values. For example, since we are scaling to 140% of the original size, over 4 seconds, we can imagine that our time line will look like this:
Because the interpolation is linear, at +1.25 seconds the value will be 1/4 of the way between 110% and 120% (112.5%). And so forth. DoubleAnimation Finally, notice that this first animation is designated as a DoubleAnimation. This indicates that the value it will alter is of type Double (and, in fact, the scaleX value is of type Double).
Returning To “Normal”
We don’t have to indicate the From value for the scale because we know that the mouseOver visual state can only be entered from one of the other CommonStates and by design they will all be at 100% of the normal value.
The Code To Change States
Silverlight provides all the code required to move among the common states and the focus states; thus you do not need, at this point, to modify MainPage.xaml.cs in any way. Of course, right now clicking OK won’t do anything; for that we’ll need an event handler, and we’ll see that in the next tutorial.
Here is the demo so far… the button has viewstates but does not have any even handling. Hover over, and click the button to see it do its thing…
The complete source code for this example is posted here.