Home > Back-end >  WPF Storyboard animation implementation for template components
WPF Storyboard animation implementation for template components

Time:10-28

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 onl oad 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:

public MarqueeTextBlockEx()
{
    Loaded  = onl oaded;
}

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 onl oaded(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?

CodePudding user response:

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.

  • Related