Text on a Path in Silverlight
November 24, 2008
New York, N.Y.
I'm normally an incorrigibly optimistic kind of guy, but as someone who has been doing WPF programming for 5 years, I'm afraid that with Silverlight I find the glass to be half empty. (By "glass" I mean of course "class library" and by "half" I mean more like "three quarters.")
Yesterday when I blogged about my article on "Text on a Path" in the latest issue of MSDN Magazine I thought I'd add a little addendum about converting the code to Silverlight. I should have realized that it wouldn't be quite that easy.
Some of the problems go very deep. Although Silverlight has UIElement and FrameworkElement classes just like WPF, there is no OnRender method to override. Nor is there a DrawingVisual class or even a DrawingContext class. Moreover, there does not seem to be any direct way to indicate that an element is a visual child of a FrameworkElement.
This means that it really makes no sense to derive from FrameworkElement because there's no way to indicate the element's visual appearance. This little issue eliminates any possibility of converting to Silverlight several of the more efficient text-on-a-path classes that I presented in that article.
Silverlight allows you to derive from Control but you can only specify the appearance of the control in a template. You can also derive from UserControl and build a visual tree as the Content property. There is also a Panel class you can derive from. However, apparently you can't write you own Panel alternative because you can't programmatically indicate that an element is a visual child of that class. There's a UIElementCollection class that at first appears to be useful for this purpose, but it has no public constructor.
My conclusion is that there are only a limited number of ways to indicate that a custom class has visual children: By deriving from Control (or one of its derivatives) and setting a template, by deriving from UserControl or ContentControl (or one of its derivatives) and setting the Content property, or by deriving from Panel (or one of its derivatives) and setting the Children property.
At first I thought my Silverlight version of text-on-a-path would derive from UserControl just like the first program I showed in the article. The TextOnPathControl class in the article defines two new properties, Text and PathFigure, and installs new property-changed handlers for the font-related properties (such as FontFamily and FontStyle). Whenever Text or any one of the font-related properties change, the class recreates TextBlock objects for each character in the string, and adds those to a Canvas set as the Content property of the UserControl. Whenever the PathFigure property changes, the class applies a transform to each TextBlock to position them on the path.
However, when it came time to define new property-changed handlers for all the font-related properties in the Silverlight version, I discovered there was no OverrideMetadata method for Silverlight dependency properties. I assume that changes to the font-related properties cause calls to MeasureOverride and ArrangeOverride, but that meant that I'd need to re-create all the TextBlock children in MeasureOverride and that just seemed wrong to me.
I decided instead to derive from Panel and add my own font-related properties. However, since Silverlight dependency properties don't have AddOwner, I had to redefine these properties from scratch, which meant that the values of these properties are not inheritable through the visual tree. Otherwise (with the exception of simple MeasureOverride and ArrangeOverride methods), the structure of this class — which I called TextOnPathPanel — is very similar to the WPF TextOnPathControl class from my article
Actually there there was even one enhancement I was able to make to TextOnPathPanel: Unlike TextOnPathControl the new class returns a correct value from MeasureOverride so that the text-on-a-path can properly participate in layout.
Here's all the source code:
The source code is a single Visual Studio solution that includes a Silverlight project and a WPF project that share source code files. I suspect a few people will be interested in this technique, so here's how I did it:
First I created a new Silverlight project in Visual Studio specifying that a solution also be created. Both the solution name and project name were TextOnPathDemo. Visual Studio actually creates two projects in the solution. The other project is named TextOnPathDemo.Web, which contains the Javascript and HTML and ASPX files. (I later deleted the ASPX files because I didn't need them.)
I then added to the solution a WPF project that I named TextOnPathDemo.Wpf. I stripped this project down to two files: App.xaml and MainWindow.xaml.
To the TextOnPathDemo project I added two new files named TextOnPathPanel.cs and PathFigureHelper.cs, and really only modified the Page.xaml and Page.xaml.cs files. Back in TextOnPathDemo.Wpf, I added the existing Page.xaml, Page.xaml.cs, TextOnPathPanel.cs, and PathFigureHelper.cs files to the project, but only as links. The MainWindow.xaml file in the WPF project references the Page class that forms the main tree of the Silverlight project. This configuration allowed me to make changes to any of these files and then run either the Silverlight version or the WPF version just by switching "Set as Startup Project" between TextOnPathDemo.Web and TextOnPathDemo.Wpf.
For the job of putting text on a path, the most significant omissions in Silverlight are GetFlattenedPathGeometry and the extremely useful GetPointAtFractionLength method. My PathFigureHelper class is devoted to compensating for these omissions. When a new PathFigure is set in the class, PathFigureHelper flattens the path and accumulates a collection of points and offsets to allow a GetPointAtFractionLength method to use a fast binary search within the collection.
I understand entirely why so much stuff is missing from Silverlight. It's a browser add-in: It has to be tiny and run on different platforms. It can't have everything in .NET 3.5. I am sure that GetFlattenedPathGeometry and GetPointAtFractionLength are comparatively esoteric methods used by only a few graphics nuts, so it's no big deal pulling these out of the Silverlight code.
But why, why, why would the Baseline property be removed from the FontFamily class? That's just cruel!
Just when I thought I had compensated for everything missing from Silverlight, it came time to animate the path used for positioning the text. I set the PathFigure property of the TextOnPathPanel object to a PathFigure containing a BezierSegment and then defined PointAnimation objects targetting the two end points and two control points. The WPF version worked fine, while the Silverlight version just sat there, as you can see:
In WPF, it is possible to define a dependency property of type PathFigure, and that property will generate property-changed notifications even when a sub-property (or a sub-sub-property, etc) changes. This is because classes such as PathFigure and BezierSegment derive from Freezable, and Freezable implements sub-property change notifications. This is the key to the extensive possibilities of WPF animation.
There is no Freezable class in Silverlight. The concept does not exist, and that is why I can't animate the path by animating the points that define the path.
But how does the Path class do it? If you set the Path class Data property to a PathGeometry that contains this same PathFigure, the Path class will be animated! I can only conclude that there's some "magic" involved — special code that lets the predefined classes work but not custom classes.
Special code that lets predefined classes work but not custom classes? That doesn't sound like the WPF I know!