Animating WPF Resources
March 25, 2007
Roscoe, N.Y.
In most common WPF and XAML applications, you don't often find yourself needing to animate a resource. Generally you define resources (such as brushes) so that you can share them among multiple elements in the XAML file, and it's not often necessary to animate something that's shared in this way. Moreover, if you really wanted to animate the shared brush, you could do so indirectly through one of the elements that use that brush.
However, there are cases where you need to define an object as a resource not because you need to share that object, but because you can't put that object directly in the markup. The resource is then referenced by an element in markup through a data binding. If you then need to animate that resource, some special considerations come into play.
For example, suppose you wanted to create a graphical animation of a Polyline that looks something like this:
The actual pattern isn't important: What's probably obvious is that a bunch of points in a PointCollection are being generated algorithmically, and the Polyline is redrawing itself based on those changing points. It's possible for a class generating these points to do so in response to a DispatcherTimer or VisualTarget event. But it's probably best to have the whole point-generation process controlled by the WPF animation system.
The algorithm to generate the PointCollection is implemented in a class named Curler. Curler has a property named Angle that indicates the degree that the points are curled. Of course, it would be most convenient to derive Curler from PointCollection and put Curler directly in a Polyline element in XAML using a Polyline.Points property element.
The problem with that brilliant idea is that PointCollection is sealed. You cannot derive from it. Instead, you need to give Curler a get-only property of type Points of type PointCollection and include Curler in your XAML file as a resource. You can define a resource of type Curler like this (assuming the XML namespace "src" has been set to the namespace and perhaps assembly containing the Curler class):
-
<src:Curler x:Key="curler" ... />
You can then reference that resource in a Polyline element like so:
-
<Polyline ... Points="{Binding Source={StaticResource curler},
Path=Points}" />
That's probably sufficient if Curler animates itself using DispatcherTimer or VisualTarget. But you probably want to use WPF animation with Curler. I mentioned that Curler has a property named Angle that governs the amount of curl, so that's the property that you'll be animating, and the property you'll reference by the Storyboard.TargetProperty attached property. But you also need to set the Storyboard.TargetName attached property, and therein lies a problem: The Curler resource needs an x:Name attribute, and you can define an x:Name for Curler but it won't be found, and, indeed, the documentation of x:Name essentially says that it'll be ignored:
-
x:Name cannot be applied in certain scopes. For instance, items in a ResourceDictionary cannot have names, because they already have the x:Key Attribute as their unique identifier.
Can you instead use the x:Key name itself for the Storyboard.TargetName property? No, you cannot. So what do you do?
This problem drove me nuts until I realized the not-so-obvious solution: Define a Name property in the Curler class. You can actually give this property any name you want, but you have to indicate that name in an attribute for the Curler class:
-
[RuntimeNameProperty("Name")]
You'll also need a using directive for System.Windows.Markup or precede RuntimeNameProperty with that namespace.
In the XAML resource, you can use the same name for x:Key and Name, and if you get a compiler error that says "Because 'Curler" is implemented in the same assembly, you must set the x:Name attribute rather than the Name attribute," that's OK. Just use x:Name in the resource and it'll work:
-
<src:Curler x:Key="curler" x:Name="curler" />
If Curler has an property named Angle that you want to animate, Angle must be backed by a dependency property and at the very least, Curler must derive from DependencyObject. For the binding between the Points property of Curler and the Points property of Polyline, you'll also want to back Points with a dependency property, and set Points from the PropertyChangedCallback method for Angle. (Don't worry: Downloadable source code is coming up.) The CLR property for Points can have a public get accessor and a protected or private set accessor so the Points property can't be set from outside the class.
The structure of the Angle and Points properties in the Curler class is characteristic of classes that have animatable properties, and it's vital to get the hang of it, and to understand how it's different from code you might write for WinForms. In a WinForms program, if you have one property (Angle) whose value affects a get-only property (Points), the set-accessor of the Angle property would respond to changes in the Angle value by firing a notification event named PointsChanged. Anybody using the class would then access the Points property to obtain the new value, and the Points get-accessor would perform the calculation. This is a type of "pull" architecture. WPF implements a "push" architecture: When the Angle property changes (which the Curler class is aware of through the PropertyChangedCallback method set through the PropertyMetadata of the Angle dependency property), the Curler class sets the resultant Points property at that time.
The PropertyChangedCallback method that sets the Points property when the Angle property changes will be frequently called when the Angle property is animated, which means it shouldn't make any memory allocations, which means that all objects that it needs should be created in advance as fields. (Realistically speaking, the method can certainly make a few memory allocations when the animation is starting up, such as those involved with allocating sufficient space for something like a PointCollection but it definitely must not automatically allocate something on each call; the last thing you want is for the CLR garbage collector to interfere with your animations.) It's also best when animating the items in a freezable collection (like PointCollection) to avoid notifications with each new object added in a for loop. For this reason, the method in Curler that fills the PointCollection begins by getting a reference to the collection from Points and then sets the Points property to null so the collection can be filled in peace. When the collection is ready, then it's set to the Points property.
If all the properties in a class such as Curler are dependency properties (remember to back Name with a dependency property as well!) then you can derive the class from Animatable. Because Animatable inherits from Freezable you need to override the CreateInstanceCore method and just call the class's constructor and return the value. If you have properties that aren't packed by dependency properties, then deriving from Freezable is much more difficult than just providing CreateInstanceCore.
Deriving Curler from Animatable gives the class ApplyAnimationClock and BeginAnimation methods which make the class usable from code in a non-storyboard animation.
Source code for the CurlAround project is downloadable here. Before compiling, select project properties, then the Signing tab, and click the Create Test Certificate button.