Animating control Content between States

You’re probably very familiar with Content in WPF and Silverlight. For example, two Buttons can use the same Template but can look different by virtue of having different Content in them. And the Content property is of type object so it can be Text, or an Image, or a Panel containing yet more elements, or anything else. In the Template, the uncertainty of what the Content might be is taken care of by the ContentPresenter element which is analogous to a TV screen showing whatever Content happens to be in the control instance.
This generality of Content is a good thing. But it does give rise to an issue in scenarios where you would like your Template (or Skin, if you prefer) to alter some aspect of the Content in a visual state. Say you’d like the Content’s Stroke color to animate to red during MouseOver. Naturally, that requirement only makes sense as long as the Content has a Stroke property.

A safe, but limited, solution is to make use of the small number of properties that ContentPresenter has. For example, in WPF, the Template might respond to an IsMouseOver Trigger by animating the ContentPresenter’s OpacityMask to Red. Then the Templated control instance could bind some property of its Content to that OpacityMask like this:

<Button Style="{DynamicResource ButtonStyle1}">     <Path Stroke="{Binding Path=OpacityMask, RelativeSource={RelativeSource AncestorType={x:Type ContentPresenter}, Mode=FindAncestor}}" Data="… "/> </Button>

That approach is limited because you soon run out of ContentPresenter properties. Another approach is for the Template to make assumptions about what will be in the the Templated control instance’s Content. You can try this in a Silverlight 2 project in the Blend 2.5 June Preview. Draw a Button and then draw a Path into the Button’s Content. Then edit a copy of the Button’s Template, select the MouseOver state, select the ContentPresenter and change its Background to Red. Now you’ll need to do some XAML editing. In the XAML, find the MouseOver state and the Background animation and change the Storyboard.TargetProperty attribute’s value to "(ContentPresenter.Content).(Shape.Stroke).(SolidColorBrush.Color)".

Build and run and you’ll see the result. This is flexible, but it is also unsafe in the sense that if a control instance uses your Template without satisfying the Template’s assumptions (say its Content is a TextBlock instead of a Shape) then a run-time error will result. However, so long as you’re careful to only use this Template on control instances whose Content is a Shape then you’ll be fine.

Corrina Barber came up with a scenario which is not addressed by the above approaches. The scenario is that of a Button which, in its Normal state, shows a still image (say, vector art of the MSN butterfly) and, in its MouseOver state, shows an animation (say, an image sequence or video of the butterly exercising its wings). Naturally, each Button instance should be able to specify the Normal and the MouseOver Content. This can’t be done by simply animating existing types’ properties in states so in this post I’ll show some of the principles of my solution to that requirement.

I wrote a fairly simple subclass of ContentControl and I called it FlipContent:

[System.Windows.Markup.ContentProperty("UnflippedContent")] public class FlipContent : ContentControl {     public static readonly DependencyProperty IsFlippedProperty =         DependencyProperty.Register(         "IsFlipped",         typeof(double),         typeof(FlipContent),         new PropertyMetadata(new PropertyChangedCallback(IsFlippedChanged)));     public double IsFlipped     {         get { return (double)GetValue(IsFlippedProperty); }         set { SetValue(IsFlippedProperty, value); }     }     public static void IsFlippedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)     {         FlipContent o = d as FlipContent;         if (o != null)         {             if (o.IsFlipped > 0)             {                 o.Content = o.FlippedContent;             }             else             {                 o.Content = o.UnflippedContent;             }         }     }     public object UnflippedContent     {         get { return _unflippedContent; }         set { _unflippedContent = value; }     } object _unflippedContent = null;     public object FlippedContent     {         get { return _flippedContent; }         set { _flippedContent = value; }     } object _flippedContent = null; }

The value proposition of the FlipContent type is that it can be made to flip its Content in reponse to a property change, therefore it can be made to flip its Content in reponse to a state change in a control template. Further, the Template needs to know nothing about the objects which constitute the FlipContent’s Content; the control instance contains those:

<Button>     <local:FlipContent>         <local:FlipContent.UnflippedContent>             <Ellipse Fill="#FFFFFFFF" Stroke="#FF000000"/>         </local:FlipContent.UnflippedContent>         <local:FlipContent.FlippedContent>             <Rectangle Fill="#FFFFFFFF" Stroke="#FF000000"/>         </local:FlipContent.FlippedContent>     </local:FlipContent> </Button>

You can try this in a Silverlight 2 project in the Blend 2.5 June Preview. Paste the class definition given above into Page.xaml.cs then add the Button XAML above. Then edit a copy of the Button’s Template, select the MouseOver state, select the ContentPresenter and change its Opacity to 50%. Now you’ll need to do some XAML editing. In the XAML, find the MouseOver state and the Opacity animation and change the Storyboard.TargetProperty attribute’s value to "(ContentPresenter.Content).(local:FlipContent.IsFlipped)". Now press F5 to test the result. You experiment with different content, and you will be able to use Blend to add and edit UnflippedContent because it is the default content property. But you won’t get as much help with FlippedContent.

You can download the project source files from below:


folderfiles Download Project Files

Please note that the solution to Corrina’s exact requirement (switching an image for some video) will probably call for a FlipMediaElementSource class which assumes its content is a MediaElement and which switches its Source property instead of its own Content property. I’ll leave that as an exercise for the reader – you maybe!

-Steve