Nov 5, 2008

Viewbox control in Silverlight Toolkit


Viewbox in Silverlight Toolkit is a very handy control that can scale its Child UIElement according to its Stretch and StretchDirection properties. It was originally written by Ted Glaza, then I took over and modified it for the release. You can read its overview or play with the sample on Silverlight Toolkit codeplex page. Bill Reiss's blog post Viewbox in the Silverlight Toolkit is a good read too. Since it was written to be as compatible as possible with the Viewbox controls in WPF, so the msdn help on WPF Viewbox can be referenced too, but with some differences between the WPF and Silverlight Toolkit implementations that I will discuss later in the post.

Below is a screenshot of Viewbox in Blend:

Viewbox in Blend


Viewbox is a simple control, or it should be :-) To over-simplify it, the implementation defines two DependencyProperty Stretch and StretchDirection, and a ContentProperty Child of UIElement type. It overrides MeasureOverride and ArrangeOverride, both of which call ComputeScaleFactor, to calculate the appropriate scale factor, based on the Child's DesiredSize, Viewbox's available/final size in measure/arrange phase, and the Stretch and StrechDirection properties, and then set the calculated ScaleTransform as Child's RenderTransform.

Only if the world is that simple :-) Below is Viewbox's implementation in WPF:

WPF Viewbox

As shown above, in WPF, Viewbox inherits from Decorator class, which doesn't exist in Silverlight. So we can either move up the inheritance chain and have Viewbox inherit from FrameworkElement, or move sideways, like inheriting from Control or Panel. Inheriting from FrameworkElement is a non starter, because there is no AddVisualChild/RemoveVisualChild/AddLogicalChild/RemoveLogicChild methods in Silverlight, so there is no way for us to render Viewbox's Child element. So the only viable option is to move sideways and find a class that can add Child element to the Visual tree. We picked Control, which allows a default Template, where we can stick a ContentPresenter to render/scale the Child element. So below is the Viewbox implementation in Silverlight Toolkit:

Viewbox in Silverlight Toolkit

But there are downside of the hack:

  • It is not 100% compatible with Viewbox in WPF. Given the constraints on Silverlight, this goal is not achievable any way. The best we can do is to be as close as possible.
  • By subclassing Control, Viewbox inherits all Control properties, like Background, BorderBrush, BorderThickness, FontFamily, FontSize, fontStretch, FontStyle, FontWeight, Foreground, HorizontalContentAlignment, VerticalContentAlighment. We decided to ignore those properties, to avoid people getting in the habit of expecting those properties as part of Viewbox interface. If you want to set those properties, you can set them on the Child element. IsEnabled property, inherited from Control, does still work for Viewbox.
  • Because we use the default template below to render Child, it creates several issues:

private const string DefaultTemplateMarkup = "<ControlTemplate xmlns=\"\" xmlns:x=\"\"><ContentPresenter x:Name=\"Child\" HorizontalAlignment=\"{TemplateBinding HorizontalAlignment}\" VerticalAlignment=\"{TemplateBinding VerticalAlignment}\"/></ControlTemplate>";

    • User can't set the Template property. Since Viewbox depends on the default template (or more precisely, the ContentPresenter in the default template), it throws exception if user tries to set its Template property. This is a particularly bad user experience if you do that inside Blend, as in the Blend screenshot above. I don't know how to disable the "Edit Control Parts(Templates)" menu in Blend. An alternative is to add a TemplatePart, but since Template shouldn't be Viewbox's interface to begin with, we didn't go down this route.
    • Because Child is rendered via the ContentPresenter in default template, x:Name doesn't work for child element. For example, if you have xaml like the one shown above in Blend screenshot: <controls:Viewbox x:Name="vbx"><Button x:Name="btn"></controls:Viewbox>, you can't reference the button via variable btn. This is because x:Name is implemented by InitializeComponent in generated *.g.cs file with code like this: this.btn = ((System.Windows.Controls.Button)(this.FindName("btn"))), and FindName doesn't work across namespaces, so btn will always be null.
    • An interesting finding is that sometimes the Child element returns zero DesiredSize, like <image source="" />, so Viewbox calculates a zero ScaleTransform, and you won't see the image at all. Doesn't always repro, not sure why.

Anyway, hope this helps you use Viewbox, and more importantly, understand its implementation, design tradeoffs, and issues, since there have been several comments in Silverlight Toolkit forum asking about above mentioned side effects. Comments, feedbacks, suggestions are welcome! Thanks!