Target Multiple Objects with one Animation (Silverlight)


What if you want to use a single animation object (let's say DoubleAnimation) to target multiple objects? 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.


This content is also covered in my MSDN topic: Working with Animations Programmatically (Silverlight 2).


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.


    <StackPanel Orientation="Horizontal">
        <StackPanel.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>
        </StackPanel.Resources>
        <Rectangle
       x:Name="MyAnimatedRectangle1"
       Margin="3" Width="100" Height="100" Fill="Blue"
       MouseLeftButtonDown="Start_Animation" />

        <Rectangle
       x:Name="MyAnimatedRectangle2"
       Margin="3" Width="100" Height="100" Fill="Blue"
       MouseLeftButtonDown="Start_Animation" />

        <Rectangle
       x:Name="MyAnimatedRectangle3"
       Margin="3" Width="100" Height="100" Fill="Blue"
       MouseLeftButtonDown="Start_Animation" />

        <Rectangle
       x:Name="MyAnimatedRectangle4"
       Margin="3" Width="100" Height="100" Fill="Blue"
       MouseLeftButtonDown="Start_Animation" />
    </StackPanel>


// C# code.

public void Start_Animation(object sender, MouseEventArgs e)
{

// If the Storyboard is running and you try to change
// properties of its animation objects programmatically,
// an error will occur.
myStoryboard.Stop();

// Get a reference to the rectangle that was clicked.
Rectangle myRect = (Rectangle)sender;

// Change the TargetName of the animation to the name of the
// rectangle that was clicked.
myDoubleAnimation.SetValue(Storyboard.TargetNameProperty, myRect.Name);

// Begin the animation.
myStoryboard.Begin();
}


Run this sample.


In the previous 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 code looks for a Storyboard that is not currently in use and uses that one to create the animation.


    <StackPanel Orientation="Horizontal">
      <StackPanel.Resources>
        <Storyboard x:Name="myStoryboard1" Completed="Storyboard_Completed">
          <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="Storyboard_Completed">
          <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="Storyboard_Completed">
           <DoubleAnimation x:Name="myDoubleAnimation3"
            Storyboard.TargetProperty="Opacity"
            From="1.0" To="0.0" Duration="0:0:2"
            AutoReverse="True" />
         </Storyboard>
       </StackPanel.Resources>
       <Rectangle x:Name="MyAnimatedRectangle1"
        Margin="3" Width="100" Height="100" Fill="Blue"
        MouseLeftButtonDown="Start_Animation" />

       <Rectangle x:Name="MyAnimatedRectangle2"
        Margin="3" Width="100" Height="100" Fill="Blue"
        MouseLeftButtonDown="Start_Animation" />

       <Rectangle x:Name="MyAnimatedRectangle3"
         Margin="3" Width="100" Height="100" Fill="Blue"
         MouseLeftButtonDown="Start_Animation" />

       <Rectangle x:Name="MyAnimatedRectangle4"
         Margin="3" Width="100" Height="100" Fill="Blue"
         MouseLeftButtonDown="Start_Animation" />
    </StackPanel>


        // C#
       
bool storyboard1Active = false;
        bool storyboard2Active = false;
        bool storyboard3Active = false;

        public void Start_Animation(object sender, MouseEventArgs e)
        {
            // Get a reference to the rectangle that was clicked.
            Rectangle myRect = (Rectangle)sender;
            if (!storyboard1Active)
            {
                myStoryboard1.Stop();
                myDoubleAnimation1.SetValue(Storyboard.TargetNameProperty, myRect.Name);
                myStoryboard1.Begin();
                storyboard1Active = true;
            }
            else if (!storyboard2Active)
            {
                myStoryboard2.Stop();
                myDoubleAnimation2.SetValue(Storyboard.TargetNameProperty, myRect.Name);
                myStoryboard2.Begin();
                storyboard2Active = true;
            }
            else if (!storyboard3Active)
            {
                myStoryboard3.Stop();
                myDoubleAnimation3.SetValue(Storyboard.TargetNameProperty, myRect.Name);
                myStoryboard3.Begin();
                storyboard3Active = true;
            }
        }

        public void Storyboard_Completed(object sender, EventArgs e)
        {
            Storyboard myStoryboard = sender as Storyboard;
            switch (myStoryboard.GetValue(NameProperty).ToString())
            {
                case "myStoryboard1": storyboard1Active = false; break;

                case "myStoryboard2": storyboard2Active = false; break;

                case "myStoryboard3": storyboard3Active = false; break;
            }
        }


Run this sample.


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 concurrent 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 my earlier post Create an Animation in Code (e.g. C#, VB .NET, etc) for more information.


Nois!
Sam Landstrom - MSFT

Comments (6)

  1. Dennis van der Stelt says:

    This isn’t a real solution. The 4th object can’t be animated and you definitly don’t wanna copy the code for the storyboard three times.

    Isn’t there any other solution than doing this programmatically? I love you article on MSDN, because it tought me how to do it in code. But I want style triggers! 🙂

  2. Dan Wahlin&#39;s latest articles, Cheryl at Silverlight SDK on SL2, WS, and WCF, Sam Landstrom on using

  3. Raumornie says:

    Dennis –

    Since you’re not going to get style triggers yet (I doubt Sam’s magic wand has that much of a charge in it), you might go the route of creating a custom user control that contains the required animation.  Takes a little more overhead (though not as much as repeating all the code and xaml), but then all the animations can run concurrently if you like.  If you’re not up to speed on custom controls, <a href="http://blogs.msdn.com/sburke/archive/2008/03/22/tutorial-writing-a-templated-silverlight-2-control.aspx">this is an excellent post.</a>  You might also look to Karen Corby’s talk from MIX08.

    Hope it helps.

    AJ

  4. ImDaFrEaK says:

    I figured out a way to reuse animations (sorta).

    Build an animations object and place it in your LayoutRoot resources.

     <DoubleAnimation x:Name="ImageResize" Storyboard.TargetProperty="Width" From="0" To="100" Duration="00:00:00.4"/>

    You do not need to embed this in a storybook.

    Then after project loads add recursive code or whatever to add the details to the LayoutRoot resources. Here I look for all the rectangles within my grid control. They all get the same settings as the default animation and are added to the resources of the LayoutRoot control. The draw back is you still have to call each one by individual names, of course you can make the animations asynchronous and skip the renaming and add only one resource.  Either way, this saved me a lot of time b/c the grids in my rectangle change often. I used the load event and a preset number of rectangle to do this example. Example:

    Private Sub MainPage_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

           Dim X As Integer = 1

           For Each UI As UIElement In Me.SlideLinks.Children

               If TypeOf UI Is Rectangle Then

                   Dim da As New DoubleAnimation

                   da.To = Me.RectangleResize.To : da.From = Me.RectangleResize.From : da.Duration = Me.RectangleResize.Duration

                   Storyboard.SetTarget(da, UI)

                   Storyboard.SetTargetProperty(da, New PropertyPath(Rectangle.WidthProperty))

                   Dim SB As New Storyboard

                   SB.Children.Add(da)

                   Me.Resources.Add("RectangleResize" & X.ToString, SB)

               End If

           Next

       End Sub

    Now I call them in any event I choose, I chose a label mousemove event.

    I used this to build a re-usable Image Viewer.  When the user moves a mouse of the label name or number of the image it slides out.  Hope this helps some people.

  5. ImDaFrEaK says:

    Sorry, I forget to include the code to activate the storyboard which isn’t hard. In the label mousemove event i put this.

    DirectCast(Me.Resources("RectangleResize1"), Storyboard).Begin()

  6. ImDaFrEaK says:

    Forgive me again, I didn’t put the x+=1 in there.  I made this short for this forum and left out that line…

    For Each UI As UIElement In Me.SlideLinks.Children

               If TypeOf UI Is Rectangle Then

                   Dim da As New DoubleAnimation

                   da.To = Me.RectangleResize.To : da.From = Me.RectangleResize.From : da.Duration = Me.RectangleResize.Duration

                   Storyboard.SetTarget(da, UI)

                   Storyboard.SetTargetProperty(da, New PropertyPath(Rectangle.WidthProperty))

                   Dim SB As New Storyboard

                   SB.Children.Add(da)

                   Me.Resources.Add("RectangleResize" & X.ToString, SB)

                   X += 1 <— I FORGOT THIS

               End If

           Next

Skip to main content