WPF Storyboard Animations – part one

Storyboard

In my previous post I described how DataTrigger and MultiDataTrigger can be used to customise the style of a UserControl . Now I want to expand on the usage of these triggers, specifically to control Storyboard  animations. In this post I will describe how to achieve this using dependency properties (DP) and logic in the XAML code-behind. In the next post I will detail how to move the logic out of the code-behind into a ViewModel in accordance with the Model-View-ViewModel pattern (MVVM).

The Demo Application

I created a small program for scrolling image panels to demonstrate controlling Storyboards using triggers. The main panel displays one image at a time and the user manually scrolls the images left and right by left clicking on a scroller control, which also indicates the total number of panels. See the video below for a demonstration:

The main panel control consists of two images; one for the currently displayed (on-screen) image and one for the previously displayed (off-screen) image:

Scrolling

For simulating scrolling I update the source data of each image at the start of a scroll through the specified bindings ( OffScreenImageSource  and OnScreenImageSource ). Then I translate both images either left or right by the width of the panel via a Storyboard  animation. The Grid clips the final image so you only ever see a panel’s width worth of data. For each image I label the TranslateTransform  property so that I can adjust it in the Storyboard  animation. I use OffScreenImageTT  and OnScreenImageTT  to label the transform of both images.  The Storyboard  animations target the x-component of the labelled transforms. Interpolating between the x-component’s current value and a new value over a particular duration is achieved using EasingDoubleKeyFrames. In this case I want to update the on-screen image by scrolling the new image right by the width of a panel, from being off-screen, in 0.15 seconds:

To complete the scroll action, I translate the old image off-screen by the same amount in the same amount of time:

Before invoking the animations above I need to update the source that each Image control is bound to (i.e. the data stored in OnScreenImageSource  and OffScreenImageSource ) by storing the currently selected pane. This logic is in the code-behind (not very MVVM but we will cover that in the next post):

The application can scroll through any number of images for the panels.  These could come from anywhere.  For example they could be stored in a database that the application queries. However for the sake of simplicity in this demo I load them from an an XML file using an XML Serializer.

Triggering Storyboards

With the Storyboards all setup I can trigger them from XAML using either a DataTrigger  or a MultiDataTrigger .  In this instance I want to trigger a particular Storyboard  based on multiple conditions, so I use a MultiDataTrigger :

When the required conditions are met, the IsScrolling  DP is set to prevent any further scrolling until the animation is complete.  The EnterActions property contains a collection of TriggerActions that are executed once the trigger has been activated.  I add the relevant  BeginStoryboard  action for invoking a scroll in the desired direction.  You may have noticed the WrapScroll  DP.  This is set to true when scrolling involves wrapping around from left end to right end or vice versa.

Attaching Triggers

There are only three places in WPF where triggers can be attached: elements, styles and templates.  In this instance attaching the triggers in a Style is ruled out as it requires invoking Storyboard  animations that target named elements (i.e. OnScreenImageTT  and OffScreenImageTT ).  Data triggers cannot be attached directly to elements .  Therefore these data triggers need to be attached in a template.  In this example I use a ControlTemplate :

Scroller Control

Since only one panel is on display at a time, it isn’t immediately obvious how many panels can be scrolled.  Therefore I created another control that represents each panel with a radio button.  This scroller control is required to display an arbitrary number of radio buttons, based on the number of panels loaded from the XML file.  To achieve this I used an ImageBrush .  An ImageBrush  is a type of TileBrush  that can display images as tiles.  By setting the ImageBrush  to stretch to fill the space in the Grid  cell that it resides, I can dynamically update the number of radio buttons through a binding. Below I bind the total width of the Grid  to the TotalWidth  DP.  This DP is calculated based on the width of a single button and the total number of panels in the code-behind.  The parent  Grid is defined by a single row and three columns where the end columns are the width of a single radio button for the arrow controls (see below), and the middle column fills the remaining space.  I specify the position and size of each tile through the Viewport  property.  The Viewbox  property enables me to specify the zoom factor of the image.  By setting the ViewportUnits  to Absolute I can then set the width and height of the Viewport  to be that of a single radio button resulting in the tiling effect.  To add a margin around each radio button I use the Viewport  starting position and the Viewbox  zoom factor.  The Margin , Width  and Height  properties of the Image  control need to be calculated based on the values of both the Viewport  and Viewbox  properties of the ImageBrush,  so that the highlighted radio button Image  is the same size as each radio button in the ImageBrush .

The control is bookended by arrow controls that react to mouse input for scrolling.  Initiating a scroll was achieved by hooking up a custom routed event to the ButtonClickAction  of each arrow control in the code-behind:

Summary

Triggers are a very useful way to manipulate a UI based on a particular condition.  In this case I was playing Storyboard  animations upon activating a trigger.  Previously I have shown how to change control styles through triggers.  They can also be used to set the values of DPs.  I have only been concentrating on data triggers, however there are others such as the EventTrigger  which can trigger an action based on an event being fired (e.g. MouseEnter  event).  There is also the standard Trigger  which automatically resets the value of the property that it has changed once the condition is no longer met.

All of the logic that resided in the XAML code-behind made me uneasy and had a bit of “code smell” about it. Code-behind logic is difficult to test and potentially incurs unnecessary overhead if the UI ever changes.  While this UI is very unlikely to change, as it was created specifically for this Blog post, real world UIs change regularly where new features are added over time.  Having the UI tied to logic in the code-behind file can incur a higher cost for modification since both the logic and the UI might have to change.  This prompted the adoption of the MVVM pattern and removing all logic from the code-behind files.  I will cover this in the next post and also briefly describe how Expression Blend can be used to fill in the gaps in vanilla WPF for adopting the MVVM pattern.  I will also provide a link to the code for the demo application.

Tagged , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *