1

EDIT 1 : In order to satisfy "Complete, Minimal And Verifiable" Example Requirement

TL:DR; Storyboard doesn't animate at all. Why?

I am attempting to create a storyboard which will animate the offsets of all the gradient stops within a gradient, shifting them from the left to the right.

I'm certain this is just a stupid syntax or argument error or something someplace on my part but I can't find it.

This is the XAML :

<Window
    x:Class="GradientShifting.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:GradientShiftDerping"
    mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"
    AllowsTransparency="True" WindowStyle="None">
    <Window.Background>
        <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
            <GradientStop Color="Black" Offset="0"/>
            <GradientStop Color="White" Offset="1"/>
        </LinearGradientBrush>
    </Window.Background>
</Window>

This is the code behind :

using System;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace GradientShifting {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        private Storyboard _sbGradientShifter = new Storyboard( );          
        public MainWindow( ) {
            InitializeComponent( );
            this.Loaded += new RoutedEventHandler(
                ( S, E ) => this.SetupGradientShift( ) );
        }

    private void SetupGradientShift( ){
        GradientBrush BackBrush = this.Background as GradientBrush;
        if ( BackBrush != null ) {
            /* Ordering by offset is important because
               the last color in the gradient requires
               special consideration. */
            DoubleAnimationUsingKeyFrames DAUKF;
            GradientStopCollection GSC = new GradientStopCollection(
                BackBrush.GradientStops.OrderBy( GS => GS.Offset ) );
            foreach( GradientStop GS in GSC ){
                DAUKF = new DoubleAnimationUsingKeyFrames( ) {
                    KeyFrames = new DoubleKeyFrameCollection( ){
                        new LinearDoubleKeyFrame(
                            1.0D, KeyTime.FromPercent( 1.0D )
                }, Duration = TimeSpan.FromSeconds( 3 )
            };

            //Something I am doing from here...
            this._sbGradientShifter.Children.Add( DAUKF );

            Storyboard.SetTarget( DAUKF, GS );

            Storyboard.SetTargetProperty(
                DAUKF, new PropertyPath( GradientStop.OffsetProperty ) );
        }
        this._sbGradientShifter.Begin( this ); //THIS DOES NOTHING.         
    }
}

So, again - this code doesn't work. I have been able to start the the animation included within the storyboard by calling GradientStop.BeginAnimation, however Storyboard.Begin does not work.

Community
  • 1
  • 1
Will
  • 3,413
  • 7
  • 50
  • 107
  • It can be very tricky to correctly assemble the necessary configuration in code-behind. It's often much easier in XAML, more maintainable as well, and that's the preferred approach. If you really want help with your code-behind version, please provide a good [mcve] that reliably reproduces the specific problem, i.e. the code you think _should_ work but doesn't. I don't think the "ugly hack" (as you put it) is as useful, but if you think it belongs in the question, please make sure it's an entirely separate code example. – Peter Duniho Jul 23 '16 at 05:10
  • Also post what goes inside `DAUKF` because this is getting added to the SB. – AnjumSKhan Jul 23 '16 at 05:19
  • @PeterDuniho See Edit for your Minimal, Complete and Verifiable Example. – Will Jul 23 '16 at 05:31
  • Possible duplicate of http://stackoverflow.com/questions/5031866/storyboard-targetting-multiple-objects-using-settarget-method-doesnt-work – Peter Duniho Jul 23 '16 at 07:04
  • I have edited your post so that the code you posted compiles. Normally, editing code in a question is frowned upon, because it risks changing the question to something other than what the author intended to ask. But in this case, I assume that whatever code you're actually dealing with, it _does_ compile and for whatever reason you didn't bother to create an actual [mcve] for inclusion in your question that you could just copy/paste in. For future reference, please make sure code in your question is _always_ a straight copy/paste from an actual working example. – Peter Duniho Jul 23 '16 at 07:24
  • see https://stackoverflow.com/questions/3377271/wpf-animation-problem-when-using-storyboard-from-code – afruzan Jul 15 '19 at 20:38

2 Answers2

2

For some reason, Storyboard.SetTarget only works with FrameworkElements or FrameworkContentElements. To do what you want, you can either start the individual animations yourself as you have in your "hack" (a perfectly reasonable way of doing animations, IMO).

Or you can register names for all your targets, e.g.:

foreach (var gs in gsc)
{
    var name = "GS_" + Guid.NewGuid().ToString("N");
    RegisterName(name, gs);
    Storyboard.SetTargetName(caukf, name);
}

If you decide to invoke the animations directly, you really don't need to save them in a separate list. Just start them immediately in the first loop as soon as they are created.

Storyboards are great if you need more coordination, such as pausing animations, using name scopes, advanced timing or animate from XAML. But in your case it seems simple Timelines would be adequate.

Eli Arbel
  • 22,391
  • 3
  • 45
  • 71
  • This is just proof of concept. I will need to be able to start ( and stop ) on a dime the animations. Being able to do it all at once would be helpful. In addition, I also require more control as well since there could be gradient stops added ( or removed ) at any time to ( or from ) the collection, in addition to how their stop locations could be changed and... it's just a big mess, but I'm very certain your answer is the one for which I am looking. – Will Jul 23 '16 at 14:03
1

As noted in the other answer, this is an undocumented (as far as I know) limitation of WPF. Call it a bug. See previous posts such as Storyboard targetting multiple objects, using SetTarget method, doesn't work and Why don't these animations work when I'm using a storyboard? for additional details.

You can generate names dynamically as noted in Eli's answer. Other alternatives include specifying the names in XAML and then referencing them in the code-behind, or just declaring the entire thing in XAML. In all cases, you'll have to use the Storyboard.TargetName property instead of the Target property.

If you want to specify the names in XAML, there are a couple of ways you can use them in code-behind: you can hard-code the names explicitly, or you can look them up as you need them. The former would be appropriate if you had to deal with just the one animation and knew the names would not change. The latter would be appropriate if you want to apply a general-purpose algorithm to multiple scenarios.

Hard-coded:

private void SetupGradientShift()
{
    string[] names = { "stop1", "stop2" };

    foreach (string name in names)
    {
        DoubleAnimationUsingKeyFrames daukf =
            new DoubleAnimationUsingKeyFrames
            {
                KeyFrames =
                    new DoubleKeyFrameCollection
                    {
                        new LinearDoubleKeyFrame(1.0, KeyTime.FromPercent(1.0))
                    },
                Duration = TimeSpan.FromSeconds(3)
            };

        this._sbGradientShifter.Children.Add(daukf);
        Storyboard.SetTargetName(daukf, name);
        Storyboard.SetTargetProperty(
            daukf, new PropertyPath(GradientStop.OffsetProperty));
    }

    this._sbGradientShifter.Begin(this);
}

Look-up at runtime:

private void SetupGradientShift()
{
    GradientBrush BackBrush = this.Background as GradientBrush;
    if (BackBrush != null)
    {
        INameScopeDictionary nameScope = (INameScopeDictionary)NameScope.GetNameScope(this);

        foreach (GradientStop gradientStop in BackBrush.GradientStops.OrderBy(stop => stop.Offset))
        {
            DoubleAnimationUsingKeyFrames daukf =
                new DoubleAnimationUsingKeyFrames
                {
                    KeyFrames =
                        new DoubleKeyFrameCollection
                        {
                            new LinearDoubleKeyFrame(1.0, KeyTime.FromPercent(1.0))
                        },
                    Duration = TimeSpan.FromSeconds(3)
                };

            this._sbGradientShifter.Children.Add(daukf);

            string name = nameScope.First(kvp => kvp.Value == gradientStop).Key;

            Storyboard.SetTargetName(daukf, name);
            Storyboard.SetTargetProperty(
                daukf, new PropertyPath(GradientStop.OffsetProperty));
        }

        this._sbGradientShifter.Begin(this);
    }
}

Either way, you would need to declare the name in XAML:

<Window.Background>
  <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
    <GradientStop x:Name="stop1" Color="Black" Offset="0"/>
    <GradientStop x:Name="stop2" Color="White" Offset="1"/>
  </LinearGradientBrush>
</Window.Background>

But personally, I think it actually would be better to just do the entire animation in XAML and leave code-behind out of it:

<Window x:Class="TestSO38537640AnimateCodeBehind.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        AllowsTransparency="True" WindowStyle="None">

  <Window.Resources>
    <Storyboard x:Key="storyboard1">
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="stop1"
                                     Storyboard.TargetProperty="Offset"
                                     Duration="0:0:3">
        <LinearDoubleKeyFrame Value="1" KeyTime="100%"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="stop2"
                                     Storyboard.TargetProperty="Offset"
                                     Duration="0:0:3">
        <LinearDoubleKeyFrame Value="1" KeyTime="100%"/>
      </DoubleAnimationUsingKeyFrames>
    </Storyboard>
  </Window.Resources>

  <Window.Background>
    <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
      <GradientStop x:Name="stop1" Color="Black" Offset="0"/>
      <GradientStop x:Name="stop2" Color="White" Offset="1"/>
    </LinearGradientBrush>
  </Window.Background>

  <Window.Triggers>
    <EventTrigger RoutedEvent="Loaded">
      <BeginStoryboard Storyboard="{StaticResource storyboard1}"/>
    </EventTrigger>
  </Window.Triggers>
</Window>
Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Unfortunately the gradient in question is defined by a control into a model, not in XAML, nor is it possible for it to be change that. For that reason I will endeavor to make use of the suggestion below to name the gradients in the code behind and see if that does the job. – Will Jul 23 '16 at 13:52