In order to start a storyboard in XAML, we need to use a BeginStoryboard element. This class extends the TriggerAction class and if you remember, that is the type that we need to use in the TriggerActionCollection of the EventTrigger class and the TriggerBase.EnterActions and TriggerBase.ExitActions properties.
We specify the storyboard to use with the BeginStoryboard element by setting it to the Storyboard property in code. When using XAML, the Storyboard property is implicitly set to the storyboard that is declared within the BeginStoryboard element.
The BeginStoryboard action is responsible for connecting the animation timelines with the animation targets and their targeted properties and is also responsible for starting the various animation timelines within its storyboard. It does this by calling the Begin method of the associated Storyboard object, once its parent's trigger condition has been met.
If an already running storyboard is asked to begin again, either indirectly, using a BeginStoryboard action, or directly, using the Begin method, what happens will depend upon the value set by the HandoffBehavior property.
This property is of the enumeration type HandoffBehavior and has two values. The default value is SnapshotAndReplace and this will renew the internal clocks and essentially have the effect of replacing one copy of the timeline with another. The other value is more interesting: the Compose value will retain the original clocks when restarting the animation and append the new animation after the current one, performing some interpolation between them, resulting in a smoother join.
One problem with this method is that the retained clocks will continue to use system resources and this can end in memory problems if not handled correctly. However, this method produces much smoother and more natural and fluid animations that can be worth the extra resources. This is best demonstrated with a small example:
<Canvas> <Rectangle Canvas.Top="200" Canvas.Left="25" Width="100" Height="100" Fill="Orange" Stroke="Black" StrokeThickness="3"> <Rectangle.Style> <Style TargetType="{x:Type Rectangle}"> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:2" Storyboard.TargetProperty="(Canvas.Top)" To="0" /> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:2" Storyboard.TargetProperty="(Canvas.Top)" To="200" /> </Storyboard> </BeginStoryboard> </Trigger.ExitActions> </Trigger> </Style.Triggers> </Style> </Rectangle.Style> </Rectangle> <Rectangle Canvas.Top="200" Canvas.Left="150" Width="100" Height="100" Fill="Orange" Stroke="Black" StrokeThickness="3"> <Rectangle.Style> <Style TargetType="{x:Type Rectangle}"> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:2" Storyboard.TargetProperty="(Canvas.Top)" To="0" /> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard HandoffBehavior="Compose"> <Storyboard> <DoubleAnimation Duration="0:0:2" Storyboard.TargetProperty="(Canvas.Top)" To="200" /> </Storyboard> </BeginStoryboard> </Trigger.ExitActions> </Trigger> </Style.Triggers> </Style> </Rectangle.Style> </Rectangle> </Canvas>
In this example, we have two rectangles, each with its own animation. The only difference between them is that the BeginStoryboard element that starts the animation for the right rectangle has a HandoffBehavior of Compose, while the other uses the default value of SnapshotAndReplace.
When the example is run, each rectangle will move upwards when the mouse cursor is placed over it and move back downwards when the cursor is moved away from it. If we keep the mouse cursor within the bounds of each rectangle, moving it up to the top of the screen with the rectangle and then move the cursor away to let the rectangle fall, the two animations will appear identical.
However, if we move the mouse cursor from side to side across the two rectangles, we will start to see a difference between the two animations. We'll see that as the cursor enters the bounds of each rectangle, they each start their upwards movement. But once the cursor leaves the rectangle bounds, we see the difference.
The rectangle on the left, with the default value of SnapshotAndReplace, will stop moving up and immediately begin its downwards animation, while the other rectangle will continue to move upwards for a short time before commencing its downwards animation. This results in a much smoother, more natural looking transition between the two animations.
The difference between these two handoff behaviors though, is most clearly demonstrated by simply placing the mouse cursor on one of the rectangles and leaving it there. Doing this to the rectangle on the left will cause the rectangle to move upwards until the mouse cursor is no longer within its bounds and then it will immediately begin to move downwards again.
However, as the mouse cursor will then be within the bounds of the rectangle again, it will begin the upwards animation once more. This will cause the rectangle to move away from the mouse cursor again and so we will end with a repetitive loop of this behavior and it will result in what looks like a quick shaking, or stuttering, of the rectangle just above the position of the mouse.
On the other hand, the rectangle on the right, with the HandoffBehavior of Compose, will move upwards until the mouse cursor is no longer within its bounds, but will then continue to move upwards for a short time before starting to move downwards again. Once more, this creates a far smoother animation and will result in the rectangle bouncing gently above the mouse cursor, in sharp contrast to the other, stuttering rectangle.
There are several related TriggerAction derived classes that are suffixed with the word Storyboard and enable us to control various aspects of the related Storyboard element. By specifying the Name property value of the BeginStoryboard element in the BeginStoryboardName property of the other actions, we are able to further control the running storyboard.
We can use the PauseStoryboard element to pause a running storyboard and the ResumeStoryboard to resume a paused storyboard. The PauseStoryboard element does nothing if the related storyboard is not running and, similarly, the ResumeStoryboard action does nothing if the related storyboard is not already paused. Therefore, a storyboard cannot be started with a ResumeStoryboard trigger action.
The StopStoryboard action will stop a running storyboard, but does nothing if the related storyboard is not already running. Finally, there is a RemoveStoryboard trigger action that will remove a storyboard when its parent's trigger condition has been met. As storyboards consume resources, we should remove them when they are no longer required.
For example, if we use an EventTrigger with the Loaded event to start a timeline that has its RepeatBehavior property set to Forever, then we should use another EventTrigger element with a RemoveStoryboard action in the Unloaded event to remove the storyboard. This is somewhat analogous to calling the Dispose method on an IDisposable implementation.
Note that it is essential to remove a storyboard that was started by a BeginStoryboard action with its HandoffBehavior property set to Compose, as it could end with many internal clocks being instantiated, but not disposed of. Removing the storyboard will also result in the internally used clocks being disposed of. Let's see a practical example of how we might use these elements:
<StackPanel TextElement.FontSize="14"> <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" Margin="20"> <TextBox.Effect> <DropShadowEffect Color="Red" ShadowDepth="0" BlurRadius="0" Opacity="0.5" /> </TextBox.Effect> <TextBox.Style> <Style TargetType="{x:Type TextBox}"> <Style.Triggers> <DataTrigger Binding="{Binding IsValid}" Value="False"> <DataTrigger.EnterActions> <BeginStoryboard Name="GlowStoryboard"> <Storyboard RepeatBehavior="Forever"> <DoubleAnimation Storyboard. TargetProperty="Effect.(DropShadowEffect.BlurRadius)" To="25" Duration="0:0:1.0" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </DataTrigger.EnterActions> </DataTrigger> <MultiDataTrigger> <MultiDataTrigger.Conditions> <Condition Binding="{Binding IsValid}" Value="False" /> <Condition Binding="{Binding IsFocused, RelativeSource={RelativeSource Self}}" Value="True" /> </MultiDataTrigger.Conditions> <MultiDataTrigger.EnterActions> <PauseStoryboard BeginStoryboardName="GlowStoryboard" /> </MultiDataTrigger.EnterActions> </MultiDataTrigger> <Trigger Property="IsFocused" Value="True"> <Trigger.EnterActions> <PauseStoryboard BeginStoryboardName="GlowStoryboard" /> </Trigger.EnterActions> <Trigger.ExitActions> <ResumeStoryboard BeginStoryboardName="GlowStoryboard" /> </Trigger.ExitActions> </Trigger> <DataTrigger Binding="{Binding IsValid}" Value="True"> <DataTrigger.EnterActions> <StopStoryboard BeginStoryboardName="GlowStoryboard" /> </DataTrigger.EnterActions> </DataTrigger> <EventTrigger RoutedEvent="Unloaded"> <EventTrigger.Actions> <RemoveStoryboard BeginStoryboardName="GlowStoryboard" /> </EventTrigger.Actions> </EventTrigger> </Style.Triggers> </Style> </TextBox.Style> </TextBox> <TextBox Margin="20 0" /> </StackPanel>
This example has two textboxes, with the lower one existing solely to enable us to remove focus from the first one. The first textbox is data bound to a Name property in our View Model. Let's imagine that we have some validation code that will update a property named IsValid when the Name property is changed. We'll cover validation in depth in Chapter 9, Implementing Responsive Data Validation, but for now, let's keep it simple:
private string name = string.Empty; private bool isValid = false; ... public string Name { get { return name; } set { if (name != value) { name = value; NotifyPropertyChanged(); IsValid = name.Length > 2; } } } public bool IsValid { get { return isValid; } set { if (isValid != value) { isValid = value; NotifyPropertyChanged(); } } }
Here, we simply verify that the Name property has a value that has three or more characters in it. The basic idea in this example is that we have an animation that highlights the fact that a particular form field requires a valid value.
It could be a shaking, or growing and shrinking of the form field, or the animation of an adjacent element, but in our case, we have used a DropShadowEffect element to create a glowing effect around it.
In the Triggers collection of our style, we have declared a number of triggers. The first one is a DataTrigger and it data binds to the IsValid property in the View Model and uses the BeginStoryboard trigger action element named GlowStoryboard to make the glowing effect around the textbox grow and shrink when the property value is false.
While animations are great at attracting the eye, they can also be quite distracting. Skipping over the MultiDataTrigger momentarily, our animation will therefore be paused when the textbox is focused, so that the user can enter the details without distraction. We achieve this by declaring a PauseStoryboard action in the trigger with the condition that the IsFocused property is true.
Using the EnterActions collection of the trigger ensures that the PauseStoryboard action is run as the IsFocused property is set to true. Declaring the ResumeStoryboard action in the ExitActions collection of the trigger ensures that it will be run as the IsFocused property is set to false, or in other words, when the control loses focus.
When the user has entered a value, our View Model validates whether the provided value is indeed valid and, if so, it sets the IsValid property to true. In our example, we just verify that the entered string contains three or more characters in order for it to be valid. Setting the UpdateSourceTrigger property to PropertyChanged on the binding ensures this validation occurs on each keystroke.
Our example uses a DataTrigger to data bind to this property and when it is true, it triggers the StopStoryboard action, which stops the storyboard from running any further. As the FillBehavior property of our storyboard is not explicitly set, it will default to the Stop value and the animated property value will return to the original value that it had prior to being animated.
However, what should happen if the user entered three or more characters and then deleted them? The data trigger would trigger the StopStoryboard action and the storyboard would be stopped. As they deleted the characters and the IsValid property would be set to false and the condition of the first DataTrigger would then trigger the initial BeginStoryboard action to start the storyboard again.
But this would occur while the focus was still on the textbox and while the animation on the effect should not be running. It is for this reason that we declared the MultiDataTrigger element that we skipped over earlier. In this trigger, we have two conditions. One is that the IsFocused property should be true and for this alone, we could have used a MultiTrigger instead.
However, the other condition requires that we data bind to the IsValid property from the View Model and for that, we need to use the MultiDataTrigger element. So, this trigger will run its PauseStoryboard action when the textbox is focused and as soon as the data bound value becomes invalid, or in other words, as the user deletes the third character.
The triggers are evaluated from top to bottom in the declared order in the XAML and as the user deletes the third character, the first trigger begins the animation. The MultiDataTrigger has to be declared after the first trigger, so that the storyboard will be started before it pauses it. In this case, the glow effect will start again once the user has moved focus from the first textbox as required.
Finally, this example demonstrates how we can use a RemoveStoryboard trigger action to remove the storyboard when it is no longer needed, freeing up its resources. The usual way to do this is by utilizing an EventTrigger in the Unloaded event of the relevant control.
While these are the only trigger action elements that control the running state of their associated storyboard elements, there are a further three actions that can control other aspects of, or set other properties of the storyboard.
The SetStoryboardSpeedRatio trigger action can set the SpeedRatio of the associated storyboard. We specify the desired ratio in its SpeedRatio property and this value will be applied when the action's related trigger condition is met. Note that this element can only work on a storyboard that has already been started, although it can work at any time after this point.
The SkipStoryboardToFill trigger action will move the current position of a storyboard to its fill period, if it has one. Remember that the FillBehavior property determines what should happen during the fill period. If the storyboard has child timelines, then their positions will also be forwarded to their fill periods at this point.
Last, but not least, there is a SeekStoryboard trigger action, which enables us to move the current position of storyboard to a location, relative to the position specified by the Origin property, which has a begin time of zero seconds by default. When declaring the SeekStoryboard action, we specify the desired seek position in the Offset property and optionally set the Origin property.
The Offset property is of type TimeSpan and we can use the time notation highlighted earlier to specify its value in XAML. The Origin property is of type TimeSeekOrigin and we can specify one of two values.
The first is the default value of BeginTime, which places the origin at the start of the timeline, while the second is Duration, which places it at the end of a single iteration of the timeline's natural duration. Note that the various speed ratio values are not taken into consideration when seeking through a timeline's duration.
That completes our look at the range of trigger actions that we can use to control our storyboards. Each of these trigger actions have corresponding methods in the Storyboard class that they call when their related trigger conditions are met.