# Friday, August 08, 2008

BabySmash Silverlight Refactorings - Part 2 - Routed Events

One of the original problems with the initial port of BabySmash to Silverlight was loading the XAML storyboard animations for UserControls.

In this post I will cover the changes required from a Silverlight point of view to replace the RoutedEvent in the UserControl.Triggers section with either equivalent XAML or C# code depending on your need.

Refactoring Routed Event Changes

XAML in WPF allows tools such as Expression Blend to define triggered events without the use of code. In the original BabySmash XAML similar the following exists, which starts a storyboard when the user control is loaded e.g.

 

<UserControl.Triggers>
    <EventTrigger RoutedEvent="FrameworkElement.Loaded">
        <BeginStoryboard Storyboard="{StaticResource EyesSB}"/>
    </EventTrigger>
</UserControl.Triggers>

 

As Shawn Wildermuth indicated on the Silverlight forums Routed Events are not available in Silverlight 2.0 Beta 2 as a result the event trigger either needs to be deleted or commented out in the XAML and alternative approach taken to achieve the same result.

Looking Richard Griffen's Silverlight Tweening Adventures with Baby Smash! MyTweenerTest it shows that the XAML can easily be refactored by moving the animations inside a BeginStoryBoard element. Hence not require the use of the Loaded event e.g.

 

<UserControl.Resources>
    <BeginStoryboard x:Name="Begin">
        <Storyboard x:Name="EyesSB" RepeatBehavior="Forever">
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="CircleEye1" Storyboard.TargetProperty="(UIElement.Opacity)" RepeatBehavior="Forever">
                <SplineDoubleKeyFrame KeyTime="00:00:02.1000000" Value="1"/>
                <SplineDoubleKeyFrame KeyTime="00:00:02.1000000" Value="0"/>
                <SplineDoubleKeyFrame KeyTime="00:00:02.300000" Value="0"/>
                <SplineDoubleKeyFrame KeyTime="00:00:02.300000" Value="1"/>
                <SplineDoubleKeyFrame KeyTime="00:00:7.300000" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="CircleEye2" Storyboard.TargetProperty="(UIElement.Opacity)" RepeatBehavior="Forever">
                <SplineDoubleKeyFrame KeyTime="00:00:02.1000000" Value="1"/>
                <SplineDoubleKeyFrame KeyTime="00:00:02.1000000" Value="0"/>
                <SplineDoubleKeyFrame KeyTime="00:00:02.300000" Value="0"/>
                <SplineDoubleKeyFrame KeyTime="00:00:02.300000" Value="1"/>
                <SplineDoubleKeyFrame KeyTime="00:00:7.300000" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </BeginStoryboard>
</UserControl.Resources>

 

However I wanted to look at a way of using the code to apply dynamic changes to the XAML and walkthough the process of refactoring.

 

Routed Event  - Replacement Using Code

 

In the next few steps I will introduce a series of refactorings that create the code behind and them reduce the amount of code that needs to be repeated.

  1. The first refactoring defines a Loaded event in each of the shapes (e.g. Circle, Heart, Hexagon, Rectangle, Square, Star, Trapezoid and Triangle)
  2. The next refactoring reduces the code by using a extension method to remove the need for the duplicated event handler implementation.
  3. The third refactoring introduces a convention that the eyes animation storyboard should be called EyesSB and applied to UserControls.

Overall the aim of the refactorings are to:

  1. Reduce the amount of code that has to be implemented by developers or designers.
  2. Provides documented implementation of how the storyboard is applied.
  3. Move the code required into a separate class that can be unit tested.
  4. Allows new shapes to be created that do not require any additional code to be written.

Overall the effect is that the Routed event section is no longer requires in each of the XAML files.

 

Refactoring 1 - Inline Event

 

Change the code behind for each of the shapes and add a ShapedLoaded event e.g.

 

public CoolHeart()
{
 this.InitializeComponent();
 this.Loaded += ShapeLoaded;
}
 
private void ShapeLoaded(object sender, RoutedEventArgs e)
{
  var eyes = FindName("EyesSB") as Storyboard;
 
  if (eyes != null)
      eyes.Begin();
}

 

Refactoring 2 - Extension Method

 

To aid with readability and reuse across all the shapes the refactoring above can further be improved. By moving to an extension method in a separate class. As a result we can remove ShapeLoaded from each shape e.g.

 

public CoolHeart()
{
 this.InitializeComponent();
 this.Loaded += this.BeginStoryBoardAnimation("EyesSB");
}

 

With the definition of the extension method as follows

 

/// <summary>
/// Helper class for XAML that performs common tasks
/// </summary>
public static class XamlHelper
{
 
    /// <summary>
    /// Begins the named storyboard animation.
    /// </summary>
    /// <remarks>No exception if thrown if the storyboard is not found</remarks>
    /// <param name="control">The control that contains the storyboard</param>
    /// <param name="storyboardName">Name of the storyboard to be started</param>
    /// <returns>An event handler instance </returns>
    public static RoutedEventHandler BeginStoryBoardAnimation(this UserControl control, string storyboardName)
    {
        return (source, args) =>
                   {
                       var eyes = control.FindName(storyboardName) as Storyboard;
 
                       if (eyes != null)
                           eyes.Begin();
                   };
    }
}

 

Refactoring 3 - Naming Convention

 

Remove the initialisation of the Loaded event from the constructor of each shape and update the FigureGenerator so that the EyesSB is started if it exists.

 

public static UserControl NewUserControlFrom(FigureTemplate template)
{
 
    UserControl retVal = null;
 
    //We'll wait for Hardware Accelerated Shader Effects in SP1
    if (template.Letter.Length == 1 && Char.IsLetterOrDigit(template.Letter[0]))
    {
          retVal = new CoolLetter(template.Fill, template.Letter);
    }
    else
    {
          retVal = template.GeneratorFunc(template.Fill);
          retVal.StartStoryboardAnimation("EyesSB");
    }

 

With the updated definition of the extension method

 

/// <summary>
 
/// Helper class for XAML that performs common tasks
 
/// </summary>
 
public static class XamlHelper
 
{
 
 
 
/// <summary>
 
/// Begins the named story board animation.
 
/// </summary>
 
/// <remarks>No exception if thrown if the story board is not found</remarks>
 
/// <param name="control">The control that contains the story board</param>
 
/// <param name="storyboardName">Name of the storyboard to be started</param>
 
/// <returns>An event handler instance </returns>
 
public static RoutedEventHandler BeginStoryBoardAnimation(this UserControl control, string storyboardName)
 
{
 
    return (source, args) => StartStoryboardAnimation(control, storyboardName);
 
}
 
 
 
/// <summary>
 
/// Starts the named story board animation.
 
/// </summary>
 
/// <remarks>No exception if thrown if the story board is not found</remarks>
 
/// <param name="control">The control that contains the story board</param>
 
/// <param name="storyboardName">Name of the storyboard to be started</param>
 
public static void StartStoryboardAnimation(this UserControl control, string storyboardName)
 
{
 
    var eyes = control.FindName(storyboardName) as Storyboard;
 
 
 
    if (eyes != null)
 
        eyes.Begin();
 
}