I Am *So* Ashamed of This Silverlight Hack
November 25, 2008
New York, N.Y.
This morning I woke up with a beautiful thought: Today I would convert to Silverlight a little XAML animation I wrote many years ago that demonstrates the MatrixAnimationUsingPath class with the DoesRotateWithTangent property set to true to make a little unicycle man travel the length of a compound Bézier curve:
Of course I knew that Silverlight didn't have a MatrixAnimationUsingPath class. Writing that class would be the fun part of the job! Obviously the WPF MatrixAnimationUsingPath class makes a call to the GetPointAtFractionLength method in the WPF PathGeometry class, and I had already implemented such a method yesterday. Moreover, in an MSDN Magazine article from last year I had explored the process of deriving new WPF animation classes, so I was in excellent shape on that front as well.
This is what Silverlight programming is all about: It's a leveraging of skills that we've acquired through years of learning all the many aspects of WPF programming.
Or maybe not. Within about 10 minutes of creating a Silverlight class named MatrixAnimationUsingPath I discovered:
-
1. All the Silverlight animation classes derive from Timeline.
-
2. Unlike the AnimationTimeline class in WPF, the Silverlight Timeline class has nothing defined as public or protected that is any way comparable to the virtual GetCurrentValue method that is the basis of creating new WPF animation classes.
In other words, the Silverlight animation classes were deliberately designed to be non-extensible, a design philosophy that seems to pervade Silverlight and to make it extremely frustrating for experienced WPF programmers. Not only is Silverlight missing many essential tools, but we can't add those tools ourselves! (On the other hand, Web programmers seem to be thrilled with Silverlight, which I suppose indicates mostly how low the bar is set in Web programming.)
I considered writing my own animation class from scratch (set a timer, get the time, compare with the animation's duration, etc) but I didn't like the idea of doing something that wouldn't quite be in sync with normal Silverlight animations.
Then I noticed that the Storyboard class has a public method named GetCurrentTime, which returns a TimeSpan. Further exploration revealed that this value reflects any SpeedRatio setting on the Storyboard, and that it stops incrementing when the Storyboard is paused.
It became clear to me that my MatrixAnimationUsingPath class could not be a child of a Storyboard but perhaps it could get access to a Storyboard, and use the GetCurrentTime method to pace the animation. The resultant MatrixAnimationUsingPath class includes not only the PathGeometry and DoesRotateWithTangent properties defined in the WPF class, but also includes a property named Storyboard intended to be set via a binding to a Silverlight Storyboard object.
When the Storyboard property in MatrixAnimationUsingPath becomes non-null, the class sets a CompositionTarget.Rendering handler, which is very much like a timer but which is triggered on the vertical retrace of the video display. The event handler calls GetCurrentTime of the Storyboard and uses that information to calculate a new Matrix object based on the position and tangent of the PathGeometry. That makes the unicycle move and rotate along the path.
The Silverlight version of UnicycleMan is here:
Notice the ToggleButton in the upper-left corner. I added this button to pause and resume the Storyboard so I could assure myself that my MatrixAnimationUsingPath responded as well.
Of course this is such a sleazy hack that I really feel reluctant about turning over the source code:
As usual, many files are generated by Visual Studio. I only messed with Page.xaml and Page.xaml.cs. The new files are MatrixAnimationUsingPath.cs and PageFigureHelper.cs (which is the same as the version in yesterday's project).
I promise I won't do anything like this again. I think it's best if we all just wait for Microsoft to decide what should and should not be in Silverlight and exactly how extensible it should be, because if there's anything we can all agree on, it's that they know best.