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):
AnimateFrameOutToLeft = true; AnimateFrameOutToLeft = false;
AnimateFrameInToLeft = true; AnimateFrameInToLeft = false;
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?
AnimateFrameOutToLeft
is fired for the second time it does not show the animationShellView.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>
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
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.
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>
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>
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With