COLLABORATING ON CREATING ANIMATIONS IN WPF AND BLEND: POSSIBILITIES AND LIMITATIONS
One area that is currently top of mind for me is designer and developer collaboration with WPF and Expression Blend, in particular around animation. I'd like to start documenting some techniques and outlining things that can and can't be done.
Before diving in, let's quickly review the animation system in WPF. The two ways to do animation in WPF is either declaratively (XAML) or procedurally (code). If you use Expression Blend to create your animations, you will be in effect using the declarative (XAML) method. What gets slightly confusing is that the two methods, declarative and procedural, are not entirely in parallel. For example, the only way to begin an animation declaratively (XAML) is through the <BeginStoryboard> tag. However, the equivalent call in code is to call the Begin method off a UIElement and passing it the storyboard to fire. The other option is to call the Begin method from a storyboard itself, passing it the UIElement to which the storyboard should be applied. And, in code, you can trigger animations that don't even have storyboards using the ApplyAnimationClock or BeginAnimation directly off a UIElement. (More on this later if you're confused. The syntax is, like much of WPF, a little tricky.)
But all of the above -- and much of the SDK documentation on animation -- assumes that you have chosen either one or the other. What happens if you want to mix and match these two methods? This particularly comes into play if you are working on a project where animations get created in Blend but then need to be manipulated, triggered and recontextualized from code. And it potentially gets more tricky if you have multiple people working on the project, the classic scenario being one person who creates the animations in Blend and another who works with those animations in code.
So, let's look at the possibilities:
1. How to trigger a storyboard from code that was created in Blend.
Let's say we have an animation in Blend to do an opacity fade on an image. Assuming it was created by creating a new timeline and leave the "Create as a resource" checkbox checked, we can get at it from code pretty easily.Simply grab the storyboard from the resources, cast it appropriately and begin it.
Storyboard s = (Storyboard) this.FindResource("FadeIn");
Now, the reason this works is that the storyboard itself contains information about the name of the object that it wants to animate. If you look at the storyboard in the sample code I posted, it sets the Storyboard.TargetName value explicitly to the name "image". Thus, when the storyboard is called from the page itself via BeginStoryboard, it is able to find the element named image and run the animations. But what happens if we want to generically apply that animation that was created in Blend to other elements?
2. How to trigger a storyboard from code and apply it to another UI element
Well, to accomplish this task, we need to start hacking XAML. If this scares you, don't read on. But the fact is that XAML generated from Blend sometimes needs a little love. So, in this case, we are going to copy that storyboard. We can't do that without jumping into the XAML and manually coping the entire <Storyboard> and giving it a new x:Key. Once we've done that, we have manually remove every instance where Storyboard.TargetName is set. Literally, remove the entire attribute. Now, we can apply this storyboard to any UIElement and not just images. In this case, we call the Begin method from the storyboard itself, passing the element we want the storyboard to be applied to.
Storyboard s = (Storyboard)this.FindResource("FadeIn2");
The thing to watch out for with this technique is to make sure you call the storyboard on an element that in fact has all the properties referenced in the storyboard. Otherwise, you'll get a runtime exception that says something like:
Additional information: '[Unknown]' property does not point to a DependencyObject in path '(0).(1)..(2)'.
Which is saying that the animation could find the property that it was told to animate. A place where this can get you is if you are doing any Transform animations, such as Scale, Translate, Rotate or Skew. By default, when you add an element to Blend, it does not create a TransformGroup for it. For example, go to Blend, add a Rectangle to the stage and see what it creates in XAML. It won't have a RenderTransform. Now, go change its scale to 2 and then change it back to 1. Now look at its XAML: a RenderTransform has been permanently added to it as a child which doesn't do anything! Blend counts on that TransformGroup for doing its animations, in that exact order (Scale, Skew, Rotate, Translate). So, if you've created an animation that does a transform and you want to apply it to some other element, be sure that the element has a TransformGroup with the four transforms.
3. How to tweak an animation created in Blend on the fly in code
Let's say there's an animation created in Blend that you want to use, but you want to actually modify it with some dynamic values at runtime, maybe adding more animation. First, you grab the storyboard and clone it so you have a new instance to party on. Then, you create your animation in code -- that's where you could insert dynamic variables. Finally, you have to create a property path to the value you want animated. The syntax is a little goofy, but once you crack its code as far as a path (good doc here) you'll be off and running. Here's the code I used to add a scale animation on the fly:
Storyboard s = ((Storyboard)this.FindResource("FadeIn2")).Clone();
DoubleAnimation da = new DoubleAnimation(0, 1, TimeSpan.FromSeconds(1), FillBehavior.HoldEnd);
Storyboard.SetTargetProperty(da, new PropertyPath("(0).(1)..(2)", UIElement.RenderTransformProperty,
You could use a similar technique to crack into the storyboard, grab an animation that was created in XAML and tweak it. So for example, we could grab the animation from the storyboard and change its begin time like this:
DoubleAnimationUsingKeyFrames d = (DoubleAnimationUsingKeyFrames)s.Children;
d.BeginTime = TimeSpan.FromSeconds(.5);
4. How to crack into an animation that is part of a DataTemplate
Doh! You can't! I tried every workaround I could, but once a ContentTemplate gets loaded, it is in the WPF terminology "Sealed" and I've never seen a way to make it unsealed, even before it is added to the visual tree. Bummer.
So where are we? We've learned some techniques such that a design technologist or interactive designer or diviner (or whatever they are called) can work in Blend, creating subtle and great animations and then, if there are scenarios where the developer or integrator or diviner (or whatever they are called) needs to start that animation, reuse that animation or tweak that animation on the fly, they can. Go WPF!