Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Frame animation to slide in/out pages is not working properly with data triggers

Goal

With my application, I try to create a "single window"-application in WPF. Therefore the content of the frame (shown below) is set through data bindings. In order to animate this process i trigger the AnimateFrameOutToLeft- and AnimateFrameInToLeft- Properties out of the ViewModel. That should create the effect of new Pages "sliding" in or out. Steps of the process of changing the frame's content (in the ViewModel):

  1. AnimateFrameOutToLeft = true; AnimateFrameOutToLeft = false;
  2. Change the content of the frame
  3. AnimateFrameInToLeft = true; AnimateFrameInToLeft = false;

Problem

In this order (regarding the data triggers) the animation of the page "sliding" is not shown. The opacity of the frame is 0. With a changed order the animation of the page "sliding" out is not shown. Why is that so? And how can I solve this?

Approaches

  • I debugged the data triggers with custom attached properties: they are all fired correctly
  • I debugged the animations (CompleteEvent) with custom attached properties: working as expected
  • Changed the order of the data triggers: when AnimateFrameOutToLeft is fired for the second time it does not show the animation

ShellView.xaml

<Frame Margin="0" Background="White" Content="{Binding FrameContent, Mode=OneWay}" Focusable="False">
    <Frame.Style>
        <Style TargetType="{x:Type Frame}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding AnimateFrameInToLeft}" Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" From="50,0,0,0" To="0" FillBehavior="HoldEnd"/>
                                <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" From="0" To="1" FillBehavior="HoldEnd"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
                <DataTrigger Binding="{Binding AnimateFrameOutToLeft}" Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" From="0" To="0,0,50,0" FillBehavior="HoldEnd"/>
                                <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" From="1" To="0" FillBehavior="HoldEnd"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Frame.Style>
   ...
</Frame>

Edit (Refering binding problems out of the ControlTemplate)

ShellView.xaml

<Window ...>
    <Window.InputBindings>
        <KeyBinding Key="Esc" Command="{Binding KeyPressedCommand}" CommandParameter="Esc"/>
    <!-- this binding works -->
    </Window.InputBindings>
    <Grid>
        <Frame Margin="0" Background="White" Content="{Binding FrameContent, Mode=OneWay}" Focusable="False">
            <Frame.ContentTemplate>
                <ItemContainerTemplate>
                    <ContentControl Content="{Binding}">
                        <ContentControl.Style>
                            <Style TargetType="{x:Type ContentControl}">
                                <Setter Property="RenderTransform">
                                    <Setter.Value>
                                        <TranslateTransform/>
                                    </Setter.Value>
                                </Setter>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding Path=DataContext.AnimateFrameToLeft, RelativeSource={RelativeSource AncestorType={x:Type local:ShellView}}}"  Value="True">
                                <!-- this binding does not work -->
...

The AnimateFrameToLeft-Property is defined in the same class as the command above

like image 425
moritzhank Avatar asked Sep 06 '25 03:09

moritzhank


1 Answers

Previous animation is blocking the following animation (or the animated properties) due to the FillBehavior.HoldEnd.

Another error is the way you manipulate the Frame.Margin property.
Note that if you want to slide out to the left your ThicknessAnimation should animate the Margin from 0 to -50,0,0,0 (instead of 0,0,50,0).
This is because a positive FrameworkElement.Margin only affects the surrounding elements and not the actual element. This means because there is no neighboring element to the right of the Frame, you don't see any effect (otherwise the right neighbor would be pushed to the right by 50 pixels).
To affect the Frame, you would have to use a negative Margin to "drag" it.
The recommended approach would be to animate a TranslateTransform instead of the margin's thickness.

Also note that animating the Frame itself may not be the best solution. If you show the navigation controls on the Frame, it would look quite odd to see everything moving in and out. You should animate the Frame.Content directly instead.

Version 1 (recommended)

The DataTrigger is based on a property's state. It is triggered by a state change and executes the DataTrigger.EnterActions. Once the state falls back to the original state, the DataTrigger.ExitActions will be executed. The "property value lock" held by the enter animation (which uses FillBehavior.HoldEnd) will not affect the exit animation.
You should make use of the automatic state tracking by the DataTrigger and move the second slide-in animation to the DataTrigger.ExitActions collection.

In the following example the Frame.ContentTemplate is used to animate the content rather than the Frame itself. Instead of a ThicknessAnimation this example uses a DoubleAnimation to animate the frame's ContentControl.RenderTransform property (which is set to a TranslateTransform):

Trigger the animation

private async void ChangePage_OnButtonClick(object sender, RoutedEventArgs e)
{
  // Slide current Frame.Content out to the left
  AnimateFrameOutToLeft = true;

  
  await Task.Delay(5000);

  // Slide new Frame.Content in from left to right left
  AnimateFrameOutToLeft = false;
}

Animation definition

<Frame x:Name="Frame" NavigationUIVisibility="Visible">
  <Frame.ContentTemplate>
    <ItemContainerTemplate>
      <ContentControl Content="{Binding}">
        <ContentControl.Style>
          <Style TargetType="ContentControl">
            <Setter Property="RenderTransform">
              <Setter.Value>
                <TranslateTransform />
              </Setter.Value>
            </Setter>

            <Style.Triggers>
              <DataTrigger Binding="{Binding AnimateFrameOutToLeft}" Value="True">
                <DataTrigger.EnterActions>
                  <BeginStoryboard>
                    <Storyboard>
                      <DoubleAnimation Storyboard.TargetProperty="RenderTransform.(TranslateTransform.X)" 
                                       Duration="0:0:0.25"
                                       To="-50" 
                                       FillBehavior="HoldEnd" />
                      <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                       Duration="0:0:0.25" 
                                       From="1" To="0"
                                       FillBehavior="HoldEnd" />
                    </Storyboard>
                  </BeginStoryboard>
                </DataTrigger.EnterActions>

                <DataTrigger.ExitActions>
                  <BeginStoryboard>
                    <Storyboard>
                      <DoubleAnimation Storyboard.TargetProperty="RenderTransform.(TranslateTransform.X)"
                                       Duration="0:0:0.25"
                                       To="0" 
                                       FillBehavior="HoldEnd" />
                        <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                         Duration="0:0:0.25" 
                                         From="0" To="1"
                                         FillBehavior="HoldEnd" />
                    </Storyboard>
                  </BeginStoryboard>
                </DataTrigger.ExitActions>
              </DataTrigger>
            </Style.Triggers>
          </Style>
        </ContentControl.Style>
      </ContentControl>
    </ItemContainerTemplate>
  </Frame.ContentTemplate>
</Frame>

Version 2

The properties Frame.Margin and Frame.Opacity are still blocked by the previous animation which is still executing due to the FillBehavior.HoldEnd setting.
You have to stop the previous animation before executing the next in the timeline.

Give the starting BeginStoryboard a name e.g. SlideOutStoryboard. Then add a StopStoryboard, that targets the former BeginStoryboard, to the slide-in trigger's EnterActions collection:

<Frame.Style>
  <Style TargetType="{x:Type Frame}">
    <Style.Triggers>
      <DataTrigger Binding="{Binding AnimateFrameOutToLeft}" Value="True">
        <DataTrigger.EnterActions>
          <BeginStoryboard x:Name="SlideOutStoryboard">
            <Storyboard>
              <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" 
                                  From="0" To="-50,0,0,0" 
                                  FillBehavior="HoldEnd" />
              <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" 
                               From="1" To="0"
                               FillBehavior="HoldEnd" />
            </Storyboard>
          </BeginStoryboard>
        </DataTrigger.EnterActions>
      </DataTrigger>

      <DataTrigger Binding="{Binding AnimateFrameInToLeft}" Value="True">
        <DataTrigger.EnterActions>

          <!-- Stop the previous BeginStoryBoard "SlideOutStoryboard" -->
          <StopStoryboard BeginStoryboardName="SlideOutStoryboard" />

          <BeginStoryboard>
            <Storyboard>
              <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" 
                                  From="-50,0,0,0" To="0,0,0,0" 
                                  FillBehavior="HoldEnd" />
              <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" 
                               From="0" To="1"
                               FillBehavior="HoldEnd" />
            </Storyboard>
          </BeginStoryboard>
        </DataTrigger.EnterActions>
      </DataTrigger>
    </Style.Triggers>
  </Style>
</Frame.Style>

Version 3

This is an implementation that extends Frame. It is used like the standard Frame except that it automatically animates the Content on navigation (or Content change):

public class AnimatedFrame : Frame
{
  private bool IsAnimating { get; set; }
  private UIElement NextContent { get; set; }
  private UIElement PreviousContent { get; set; }
  private Action PreviousContentTransformCleanupDelegate { get; set; }
  private Action NextContentTransformCleanupDelegate { get; set; }

  public AnimatedFrame() => this.Navigating += OnNavigating;

  private void OnNavigating(object sender, NavigatingCancelEventArgs e)
  {
    if (this.IsAnimating 
      || !(e.Content is UIElement nextContent 
        && this.Content is UIElement))
    {
      return;
    }

    e.Cancel = true;
    this.PreviousContent = this.Content as UIElement;
    this.NextContent = nextContent;
    AnimateToNextContent();
  }

  private void AnimateToNextContent()
  {
    PrepareAnimation();
    StartPreviousContentAnimation();
  }

  private void PrepareAnimation()
  {
    this.IsAnimating = true;


    Transform originalPreviousContentTransform = this.PreviousContent.RenderTransform;
    this.PreviousContent.RenderTransform = new TranslateTransform(0, 0);
    this.PreviousContentTransformCleanupDelegate =
      () => this.PreviousContent.RenderTransform = originalPreviousContentTransform;


    Transform originalNextContentTransform = this.NextContent.RenderTransform;
    this.NextContent.RenderTransform = new TranslateTransform(0, 0);
    this.NextContentTransformCleanupDelegate = () => this.NextContent.RenderTransform = originalNextContentTransform;
  }

  private void StartPreviousContentAnimation()
  {
    var unloadAnimation = new Storyboard();
    DoubleAnimation slideOutAnimation = CreateSlideOutAnimation();
    unloadAnimation.Children.Add(slideOutAnimation);

    DoubleAnimation fadeOutAnimation = CreateFadeOutAnimation();
    unloadAnimation.Children.Add(fadeOutAnimation);
    unloadAnimation.Completed += StartNextContentAnimation_OnPreviousContentAnimationCompleted;

    unloadAnimation.Begin();
  }

  private void StartNextContentAnimation_OnPreviousContentAnimationCompleted(object sender, EventArgs e)
  {
    this.Content = this.NextContent;

    var loadAnimation = new Storyboard();
    DoubleAnimation slideInAnimation = CreateSlideInAnimation();
    loadAnimation.Children.Add(slideInAnimation);

    DoubleAnimation fadeInAnimation = CreateFadeInAnimation();
    loadAnimation.Children.Add(fadeInAnimation);
    loadAnimation.Completed += Cleanup_OnAnimationsCompleted;

    loadAnimation.Begin();
  }

  private void Cleanup_OnAnimationsCompleted(object sender, EventArgs e)
  {
    this.PreviousContentTransformCleanupDelegate.Invoke();
    this.NextContentTransformCleanupDelegate.Invoke();
    this.IsAnimating = false;
  }

  private DoubleAnimation CreateFadeOutAnimation()
  {
    var fadeOutAnimation = new DoubleAnimation(1, 0, new Duration(TimeSpan.FromMilliseconds(250)), FillBehavior.HoldEnd)
      {BeginTime = TimeSpan.Zero};
    Storyboard.SetTarget(fadeOutAnimation, this.PreviousContent);
    Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(nameof(UIElement.Opacity)));
    return fadeOutAnimation;
  }

  private DoubleAnimation CreateSlideOutAnimation()
  {
    var slideOutAnimation = new DoubleAnimation(
        0,
        -50,
        new Duration(TimeSpan.FromMilliseconds(250)),
        FillBehavior.HoldEnd)
      {BeginTime = TimeSpan.Zero};

    Storyboard.SetTarget(slideOutAnimation, this.PreviousContent);
    Storyboard.SetTargetProperty(
      slideOutAnimation,
      new PropertyPath(
        $"{nameof(UIElement.RenderTransform)}.({nameof(TranslateTransform)}.{nameof(TranslateTransform.X)})"));
    return slideOutAnimation;
  }

  private DoubleAnimation CreateFadeInAnimation()
  {
    var fadeInAnimation = new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(250)), FillBehavior.HoldEnd);
    Storyboard.SetTarget(fadeInAnimation, this.NextContent);
    Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(nameof(UIElement.Opacity)));
    return fadeInAnimation;
  }

  private DoubleAnimation CreateSlideInAnimation()
  {
    var slideInAnimation = new DoubleAnimation(
      -50,
      0,
      new Duration(TimeSpan.FromMilliseconds(250)),
      FillBehavior.HoldEnd);

    Storyboard.SetTarget(slideInAnimation, this.NextContent);
    Storyboard.SetTargetProperty(
      slideInAnimation,
      new PropertyPath(
        $"{nameof(UIElement.RenderTransform)}.({nameof(TranslateTransform)}.{nameof(TranslateTransform.X)})"));
    return slideInAnimation;
  }
}
like image 145
BionicCode Avatar answered Sep 09 '25 02:09

BionicCode