Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

WPF Storyboard animation implementation for template components

Today I’m dealing with the problem of creating an animation for components that are created in template.

Previously I had something like this (implemented in TextBlock OnLoad Trigger):

<TextBlock.Triggers>
  <EventTrigger RoutedEvent="TextBlock.Loaded">
    <BeginStoryboard>
      <Storyboard
        x:Name="contentStoryboard"
        Storyboard.TargetName="contentText">

        <DoubleAnimation
          BeginTime="0:0:0"
          Storyboard.TargetProperty="(Canvas.Left)"
          AutoReverse="{Binding MarqueeBouncing, RelativeSource={RelativeSource AncestorType={x:Type local:MarqueeTextBlockEx}}}"
          Duration="{Binding MarqueeDuration, RelativeSource={RelativeSource AncestorType={x:Type local:MarqueeTextBlockEx}}}"
          RepeatBehavior="Forever">

          <DoubleAnimation.From>
            <MultiBinding Converter="{StaticResource StartPositionConverter}">
              <Binding Path="MarqueeStartPosition" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MarqueeTextBlockEx}}"/>
              <Binding ElementName="border" Path="ActualWidth"/>
              <Binding ElementName="contentText" Path="ActualWidth"/>
              <Binding Path="WaitForText" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MarqueeTextBlockEx}}"/>
            </MultiBinding>
          </DoubleAnimation.From>

          <DoubleAnimation.To>
            <MultiBinding Converter="{StaticResource EndPositionConverter}">
              <Binding Path="MarqueeEndPosition" RelativeSource="{RelativeSource AncestorType={x:Type local:MarqueeTextBlockEx}}"/>
              <Binding ElementName="border" Path="ActualWidth"/>
              <Binding ElementName="contentText" Path="ActualWidth"/>
              <Binding Path="WaitForText" RelativeSource="{RelativeSource AncestorType={x:Type local:MarqueeTextBlockEx}}"/>
            </MultiBinding>
          </DoubleAnimation.To>
        </DoubleAnimation>
      </Storyboard>
    </BeginStoryboard>
  </EventTrigger>
</TextBlock.Triggers>

And after I wanted to implement bool parameter to control animation on and off.
I created a code behind:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

public MarqueeTextBlockEx()
{
    Loaded += OnLoaded;
}

static MarqueeTextBlockEx()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(MarqueeTextBlockEx),
        new FrameworkPropertyMetadata(typeof(MarqueeTextBlockEx)));
}

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    
    contentBorder = GetBorder("border");
    contentCanvas = GetCanvas("contentCanvas");
    contentTextBlock = GetTextBlock("contentText");
}

protected TextBlock GetTextBlock(string textBlockName)
{
    return this.Template.FindName(textBlockName, this) as TextBlock;
}

protected virtual void OnLoaded(object sender, RoutedEventArgs e)
{
    Storyboard = CreateStoryboard();

    if (MarqueeEnabled)
        Storyboard.Begin();
}

private Storyboard CreateStoryboard()
{
    var storyboard = new Storyboard();

    var doubleAnimation = new DoubleAnimation()
    {
        AutoReverse = MarqueeBouncing,
        BeginTime = new TimeSpan(0, 0, 0),
        Duration = MarqueeDuration,
        RepeatBehavior = RepeatBehavior.Forever,
        From = GetStartPosition(),
        To = GetEndPosition()
    };

    Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Canvas.Left"));
    Storyboard.SetTarget(doubleAnimation, contentTextBlock);

    return storyboard;
}

private double GetStartPosition()
{
    var startPosition = MarqueeStartPosition;
    var canvasWidth = contentBorder.ActualWidth;
    var textWidth = contentTextBlock.ActualWidth;
    var waitForText = WaitForText;

    switch (MarqueeStartPosition)
    {
        case MarqueeTextAnimationPlace.LeftOutside:
            return -textWidth;

        case MarqueeTextAnimationPlace.LeftInside:
            return waitForText &&  IsTextTooLong(canvasWidth, textWidth)
                ? -(textWidth - canvasWidth)
                : 0;

        case MarqueeTextAnimationPlace.RightInside:
            return waitForText && IsTextTooLong(canvasWidth, textWidth)
                ? 0
                : canvasWidth - textWidth;

        case MarqueeTextAnimationPlace.RightOutside:
        default:
            return canvasWidth;
    }
}

private double GetEndPosition()
{
    var startPosition = MarqueeEndPosition;
    var canvasWidth = contentBorder.ActualWidth;
    var textWidth = contentTextBlock.ActualWidth;
    var waitForText = WaitForText;

    switch (startPosition)
    {
        case MarqueeTextAnimationPlace.LeftOutside:
            return -textWidth;

        case MarqueeTextAnimationPlace.LeftInside:
            return waitForText && IsTextTooLong(canvasWidth, textWidth)
                ? -(textWidth - canvasWidth)
                : 0;

        case MarqueeTextAnimationPlace.RightInside:
            return waitForText && IsTextTooLong(canvasWidth, textWidth)
                ? 0
                : canvasWidth - textWidth;

        case MarqueeTextAnimationPlace.RightOutside:
        default:
            return canvasWidth;
    }
}

// And of course the property for control:

public bool MarqueeEnabled
{
    get => marqueeEnabled;
    set
    {
        marqueeEnabled = value;
        
        if (Storyboard != null)
        {
            if (value)
                Storyboard.Begin();

            else
            {
                Storyboard.Stop();
                TextPosition = 0;
            }
        }
    }
}

The TextBlock component is setup in this way

<Border
  x:Name="border"
  Background="{TemplateBinding Background}"
  BorderBrush="{TemplateBinding BorderBrush}"
  BorderThickness="{TemplateBinding BorderThickness}"
  CornerRadius="{TemplateBinding CornerRadius}">

  <Grid
    x:Name="grid"
    Margin="{TemplateBinding Padding}">
                
    <Canvas
      x:Name="contentCanvas"
      ClipToBounds="True"
      Height="{Binding ActualHeight, ElementName=contentText}"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch"
      Width="{Binding ActualWidth, ElementName=grid}">

      <TextBlock
        x:Name="contentText"
        Canvas.Left="{Binding TextPosition, RelativeSource={RelativeSource AncestorType={x:Type local:MarqueeTextBlockEx}}}"
        Foreground="{TemplateBinding Foreground}"
        Height="Auto"
        HorizontalAlignment="Left"
        Text="{TemplateBinding Text}"
        VerticalAlignment="Center"
        Width="Auto"/>
    </Canvas>
  </Grid>
</Border>

What could have caused it not to work that way?

>Solution :

Add the DoubleAnimation to the Children property of the Storyboard and add parentheses around the Canvas.Left:

var storyboard = new Storyboard();

var doubleAnimation = new DoubleAnimation()
{
    ...
};

storyboard.Children.Add(doubleAnimation);
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(Canvas.Left)"));
Storyboard.SetTarget(doubleAnimation, contentTextBlock);

storyboard.Begin();

If you still cannot me it work, then please edit your question to include a minimal and reproducible Example.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading