Working with Animations Programmatically in Silverlight

There are times when you may want to change the properties of an animation dynamically or on the fly. For example, you might want to adjust the behavior of an animation that is applied to an object, depending on the object's current location in the layout, what kind of content the object contains, and so on. This overview introduces some basic techniques for manipulating animations dynamically by using JavaScript.

This topic contains the following sections:

  • Accessing Animations by Name
  • Accessing Animations by Using Collections
  • Dynamically Changing TargetName

To understand this topic, you should be familiar with Microsoft Silverlight animations. For an introduction, see the Animation Overview.

Accessing Animations by Name

The most direct way to access an animation object to change its properties is to give the animation object a name, and then refer to it in JavaScript by using the FindName method. The following example consists of an Ellipse that animates to wherever you click on the screen. To accomplish this animation, the sample dynamically changes the To property of the PointAnimation object whenever the Canvas is clicked, after which the animation is started.

XAML
<Canvas
  xmlns="https://schemas.microsoft.com/client/2007"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  Width="600" Height="500"
  Background="Gray"
  MouseLeftButtonDown="handleMouseDown">
  <Canvas.Resources>
    <Storyboard x:Name="myStoryboard">
      <!-- The PointAnimation has a name so it can be accessed
           from code. The To property is left out of the XAML
           since the value of To is determined in code. -->
      <PointAnimation
       x:Name="myPointAnimation"
       Storyboard.TargetProperty="Center"
       Storyboard.TargetName="MyAnimatedEllipseGeometry"
       Duration="0:0:2" />
    </Storyboard>
  </Canvas.Resources>
  <Path Fill="Blue">
    <Path.Data>
      <!-- Describes an ellipse. -->
      <EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
       Center="200,100" RadiusX="15" RadiusY="15" />
    </Path.Data>
  </Path>
</Canvas>
JavaScript
function handleMouseDown(sender, mouseEventArgs)
{
    // Retrieve current mouse coordinates.
    var newX = mouseEventArgs.getPosition(null).x;
    var newY = mouseEventArgs.getPosition(null).y;
    // Retrieve Storyboard.
    var myStoryboard = sender.findName("myStoryboard");
    // Retrieve the PointAnimation.
    var myPointAnimation = sender.findName("myPointAnimation");
    
    myPointAnimation.to = newX + "," + newY;
    myStoryboard.begin();
    
}

Accessing Animations by Using Collections

It might not always be practical or convenient to have unique names for all your animations. Alternatively, you can access animations or keyframes of animations by using collections. For example, if you want to programmatically access all the keyframes within a DoubleAnimationUsingKeyFrames object, you could use code similar to the following.

XAML
<Canvas
  xmlns="https://schemas.microsoft.com/client/2007"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  Width="600" Height="500"
  Background="Gray">
  <Canvas.Resources>
    <Storyboard x:Name="myStoryboard">
      <PointAnimationUsingKeyFrames
       x:Name="myPointAnimationUsingKeyFrames"
       Storyboard.TargetProperty="Center"
       Storyboard.TargetName="MyAnimatedEllipseGeometry"
       Duration="0:0:3">
         <!-- Set of keyframes -->
         <DiscretePointKeyFrame KeyTime="0:0:0" />
         <LinearPointKeyFrame KeyTime="0:0:0.5" />
         <SplinePointKeyFrame KeySpline="0.6,0.0 0.9,0.00" KeyTime="0:0:3" />
     </PointAnimationUsingKeyFrames>
    </Storyboard>
  </Canvas.Resources>
  <Path Fill="Blue">
    <Path.Data>
      <!-- Describes an ellipse. -->
      <EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
       Center="200,100" RadiusX="15" RadiusY="15" />
    </Path.Data>
  </Path>
</Canvas>
JavaScript
function handleMouseDown(sender, mouseEventArgs)
{
    // Retrieve the PointAnimationUsingKeyFrames.
    var myPointAnimationUsingKeyFrames = sender.findName("myPointAnimationUsingKeyFrames");
    var i;
    for(i=0; i< myPointAnimationUsingKeyFrames.KeyFrames.count; i++)
    {
      // Do something with each keyframe, for example, set values. Note that with the current
      // version of Silverlight, you can't change KeyTimes programmatically.
    }
}

The following example is similar to the previous one in that the ellipse follows where the user clicks on the screen, except that keyframes are used in this example. The collection of keyframes is iterated through and values are dynamically set on the keyframes so that the ellipse animates to the proper location.

XAML
<Canvas
  xmlns="https://schemas.microsoft.com/client/2007"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  Width="600" Height="500"
  Background="Gray"
  MouseLeftButtonDown="handleMouseDown">
  <Canvas.Resources>
    <Storyboard x:Name="myStoryboard">
      <PointAnimationUsingKeyFrames
       x:Name="myPointAnimationUsingKeyFrames"
       Storyboard.TargetProperty="Center"
       Storyboard.TargetName="MyAnimatedEllipseGeometry"
       Duration="0:0:3">
         <DiscretePointKeyFrame KeyTime="0:0:0" />
         <LinearPointKeyFrame KeyTime="0:0:0.5" />
         <SplinePointKeyFrame KeySpline="0.6,0.0 0.9,0.00" KeyTime="0:0:3" />
     </PointAnimationUsingKeyFrames>
    </Storyboard>
  </Canvas.Resources>
  <Path Fill="Blue">
    <Path.Data>
      <!-- Describes an ellipse. -->
      <EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
       Center="200,100" RadiusX="15" RadiusY="15" />
    </Path.Data>
  </Path>
</Canvas>
JavaScript
// Global variables that keep track of the end point
// of the last animation. 
var lastX = 200;
var lastY = 100;
function handleMouseDown(sender, mouseEventArgs)
{
    // Retrieve current mouse coordinates.
    var newX = mouseEventArgs.getPosition(null).x;
    var newY = mouseEventArgs.getPosition(null).y;
    // Retrieve Storyboard.
    var myStoryboard = sender.findName("myStoryboard");
    // Retrieve the PointAnimationUsingKeyFrames.
    var myPointAnimationUsingKeyFrames = sender.findName("myPointAnimationUsingKeyFrames");
    var i;
    for(i=0; i< myPointAnimationUsingKeyFrames.KeyFrames.count; i++)
    {
      var keyFrame = myPointAnimationUsingKeyFrames.KeyFrames.getItem(i);
      if(keyFrame.ToString() == "DiscretePointKeyFrame")
      {
        keyFrame.value = lastX + "," + lastY;
      }
      else if(keyFrame.ToString() == "LinearPointKeyFrame")
      {
        // The LinearKeyFrame has a value that is partway to the 
        // final end point. In addition, this value has to be on
        // the correct line. Therefore, you need to use the line 
        // formula y = mx + b to find the values of x and y.
        // Calculate the slope.
        var m = (newY - lastY)/(newX - lastX);
        // Calculate the y-intercept.
        var b = newY - (m*newX);
        // Set x to a third of the way to the end point.
        var intermediateX = lastX + (newX - lastX)/3;
        // Find the value y from x and the line formula.
        var intermediateY = (m*intermediateX) + b;
        // Set the keyframe value to the intermediate x and y value.
        keyFrame.value = intermediateX + "," + intermediateY;
      }
      else if(keyFrame.ToString() == "SplinePointKeyFrame")
      {
        keyFrame.value = newX + "," + newY;
      }
    }
    myStoryboard.stop();
    myStoryboard.begin();
    lastX = newX;
    lastY = newY;  
}

Note   Storyboard has a Children property that enables you to access all the animation objects within a given Storyboard.

Dynamically Changing TargetName

The most common scenario for dynamically changing the TargetName property is when you want to apply the same animation to more than one object. This is especially useful when you have a large number of objects that have similar animations applied to them. For example, you are displaying rows of images and you want to use an animation to highlight the image that currently has the mouse pointer over it. It is inconvenient and messy to have to create separate Storyboard objects for each image. It would be better to reuse the same Storyboard.

The following example has a number of rectangles that fade out and back into sight when you click them. All of these rectangles use the same Storyboard, because the DoubleAnimation that animates the Opacity changes its TargetName to whichever rectangle is clicked.

XAML
<Canvas
  xmlns="https://schemas.microsoft.com/client/2007"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Canvas.Resources>
    <Storyboard x:Name="myStoryboard">
      <DoubleAnimation x:Name="myDoubleAnimation"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2" 
        AutoReverse="True" />
    </Storyboard>  
  </Canvas.Resources>
  <Rectangle
    x:Name="MyAnimatedRectangle1"
    Canvas.Top="10"
    Canvas.Left="10"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
  <Rectangle
    x:Name="MyAnimatedRectangle2"
    Canvas.Top="10"
    Canvas.Left="120"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
  <Rectangle
    x:Name="MyAnimatedRectangle3"
    Canvas.Top="10"
    Canvas.Left="230"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
  <Rectangle
    x:Name="MyAnimatedRectangle4"
    Canvas.Top="10"
    Canvas.Left="340"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
</Canvas>
JavaScript
function startAnimation(sender, mouseEventArgs)
{
    // Retrieve the Storyboard.
    var myStoryboard = sender.findName("myStoryboard");
    // Retrieve the DoubleAnimation.
    var myDoubleAnimation = sender.findName("myDoubleAnimation");
    
    // If the Storyboard is running and you try to change
    // properties of its animation objects programmatically, 
    // an error will occur.
    myStoryboard.stop();
    // Change the TargetName of the animation to the name of the
    // rectangle that was clicked.
    myDoubleAnimation["Storyboard.TargetName"] = sender.Name;
    // Begin the animation.
    myStoryboard.begin();
}

In the previous JavaScript code, notice that you need to stop the Storyboard before you dynamically change the properties of its animation objects; otherwise, an error will occur. In this example, it might not be desirable to stop an animation on one rectangle so that the animation can start on another rectangle. Perhaps you want both animations to run at the same time. However, you cannot use the same animation object to run two separate animations at the same time, because there is only one TargetName. This does not mean that you are back to creating a separate Storyboard for every object. Instead, you need one Storyboard for each animation that you want to run concurrently (synchronously). The following example is similar to the previous one, except that it contains three Storyboard objects instead of one. When you click a rectangle, the script looks for a Storyboard that is not currently in use and uses that one to create the animation.

XAML
<Canvas
  xmlns="https://schemas.microsoft.com/client/2007"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Canvas.Resources>
    <Storyboard x:Name="myStoryboard1" Completed="storyboardCompleted">
      <DoubleAnimation x:Name="myDoubleAnimation1"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2" 
        AutoReverse="True" />
    </Storyboard>  
    <Storyboard x:Name="myStoryboard2" Completed="storyboardCompleted">
      <DoubleAnimation x:Name="myDoubleAnimation2"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2" 
        AutoReverse="True" />
    </Storyboard>
    <Storyboard x:Name="myStoryboard3" Completed="storyboardCompleted">
      <DoubleAnimation x:Name="myDoubleAnimation3"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2" 
        AutoReverse="True" />
    </Storyboard>
  </Canvas.Resources>
  <Rectangle
    x:Name="MyAnimatedRectangle1"
    Canvas.Top="10"
    Canvas.Left="10"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
  <Rectangle
    x:Name="MyAnimatedRectangle2"
    Canvas.Top="10"
    Canvas.Left="120"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
  <Rectangle
    x:Name="MyAnimatedRectangle3"
    Canvas.Top="10"
    Canvas.Left="230"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
  <Rectangle
    x:Name="MyAnimatedRectangle4"
    Canvas.Top="10"
    Canvas.Left="340"
    Width="100"
    Height="100"
    Fill="Blue"
    MouseLeftButtonDown="startAnimation">
  </Rectangle>
</Canvas>
JavaScript
var storyboard1Active = false;
var storyboard2Active = false;
var storyboard3Active = false;
function startAnimation(sender, mouseEventArgs)
{
    if(!storyboard1Active)
    {
      var myStoryboard1 = sender.findName("myStoryboard1");
      var myDoubleAnimation1 = sender.findName("myDoubleAnimation1");
      myStoryboard1.stop();
      myDoubleAnimation1["Storyboard.TargetName"] = sender.Name;
      myStoryboard1.begin();
      storyboard1Active = true;
    }
    else if(!storyboard2Active)
    {
      var myStoryboard2 = sender.findName("myStoryboard2");
      var myDoubleAnimation2 = sender.findName("myDoubleAnimation2");
      myStoryboard2.stop();
      myDoubleAnimation2["Storyboard.TargetName"] = sender.Name;
      myStoryboard2.begin();
      storyboard2Active = true;
    }
    else if(!storyboard3Active)
    {
      var myStoryboard3 = sender.findName("myStoryboard3");
      var myDoubleAnimation3 = sender.findName("myDoubleAnimation3");
      myStoryboard3.stop();
      myDoubleAnimation3["Storyboard.TargetName"] = sender.Name;
      myStoryboard3.begin();
      storyboard3Active = true;
    }
}
function storyboardCompleted(sender, eventArgs)
{ 
  
  switch (sender.name)
  {
    case "myStoryboard1": storyboard1Active = false; break;
    case "myStoryboard2": storyboard2Active = false; break;
    case "myStoryboard3": storyboard3Active = false; break;
  }
}

In the previous example, only three animations can run at the same time (equal to the number of Storyboard objects). This is fine if you do not anticipate a need for more animations, which would require more Storyboard objects. If you expect a lot of independent animations to be running at the same time, you might want to create your Storyboard objects dynamically. See Constructing Objects at Run Time for more information on creating XAML dynamically.

See Also

Constructing Objects at Run Time
Interactive Animations
Animations Overview
Overviews and How-to Topics