WPF animation only works in one direction - wpf

I'm making a "traffic light" animation controlled by radiobuttons in xaml. I'm using an ellipse style to change the color and animate the movement. The problem is, the animation only works when the ellipse is moving down (to a higher value of Canvas.Top), but doesn't work when it's moving up (to a lower value of Canvas.Top).
Here is the code:
<Window x:Class="hw05.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:hw05"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="Ellipse">
<Setter Property="Fill" Value="Red"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=RedRB}" Value="True">
<Setter Property="Fill" Value="Red"></Setter>
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(Canvas.Top)" Duration="0:0:0.400" To="83">
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding IsChecked, ElementName=YellowRB}" Value="True">
<Setter Property="Fill" Value="Orange"></Setter>
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(Canvas.Top)" Duration="0:0:0.400" To="133">
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding IsChecked, ElementName=GreenRB}" Value="True">
<Setter Property="Fill" Value="Green"></Setter>
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(Canvas.Top)" Duration="0:0:0.400" To="183">
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Canvas>
<RadioButton Name="RedRB" GroupName="One" IsChecked="True" Canvas.Left="50" Canvas.Top="100">first</RadioButton>
<RadioButton Name="YellowRB" GroupName="One" IsChecked="False" Canvas.Left="50" Canvas.Top="150">second</RadioButton>
<RadioButton Name="GreenRB" GroupName="One" IsChecked="False" Canvas.Left="50" Canvas.Top="200">third</RadioButton>
<Ellipse Name="Light" Width="50" Height="50" Canvas.Left="180" Canvas.Top="83"></Ellipse>
</Canvas>
</Window>

You should know that triggers like Triggerand DataTrigger are state based: you have an enter action to reflect the transition to the state (e.g., IsChecked: false => true) and an exit action to reflect the transition from the state (e.g., IsChecked: true => false). You are currently only defining an animation for the entering transition. So from the animation's perspective the entering state is never left. DataTrigger.EnterAction will hold the animation state until DataTrigger.ExitAction was executed => state based.
In your scenario it makes more sense to trigger the animation event based: the animation is executed when a certain event occurred (e.g., RadioButton.Checked) and on hold until the next event occurred. Each animation animates the values of the previous animation (if FillBehavior="Hold", which is the default).
The following example has moved the animation to the Canvas as this defines a common name scope of all participating elements:
<Canvas>
<RadioButton Name="RedRB" GroupName="One" IsChecked="True" Canvas.Left="50" Canvas.Top="100">first</RadioButton>
<RadioButton Name="YellowRB" GroupName="One" IsChecked="False" Canvas.Left="50" Canvas.Top="150">second</RadioButton>
<RadioButton Name="GreenRB" GroupName="One" IsChecked="False" Canvas.Left="50" Canvas.Top="200">third</RadioButton>
<Ellipse Name="Light"
Fill="Red"
Width="50" Height="50"
Canvas.Left="180" Canvas.Top="83" />
<Canvas.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked" SourceName="RedRB">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)"
Storyboard.TargetName="Light"
Duration="0:0:0.400" To="83" />
<ColorAnimation Storyboard.TargetProperty="Fill.(SolidColorBrush.Color)"
Storyboard.TargetName="Light"
To="Red" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="RadioButton.Checked" SourceName="YellowRB">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)"
Storyboard.TargetName="Light"
Duration="0:0:0.400" To="133" />
<ColorAnimation Storyboard.TargetProperty="Fill.(SolidColorBrush.Color)"
Storyboard.TargetName="Light"
To="Yellow" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="RadioButton.Checked" SourceName="GreenRB">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)"
Storyboard.TargetName="Light"
Duration="0:0:0.400" To="183" />
<ColorAnimation Storyboard.TargetProperty="Fill.(SolidColorBrush.Color)"
Storyboard.TargetName="Light"
To="Green" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
</Canvas>

Related

How to write a style for a control that changes width of another control?

Is there any way to write a Style for a control that changes width of another control?
<Style x:Key="SubMenuStyle" TargetType="Label">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="Arial"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="LightCyan"/>
<Style.Triggers>
<EventTrigger RoutedEvent="MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Menu" Storyboard.TargetProperty="Width" To="0" Duration="0:0:.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
This code leads to error:
TargetName property cannot be set on a Style Setter
I know I can write codes below and it works:
<Label Name="Owners" Margin="0,1,0,0" MouseLeftButtonDown="SubMenuClicked" Style="{StaticResource SubMenuStyle}">
<Label.Triggers>
<EventTrigger RoutedEvent="MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Menu" Storyboard.TargetProperty="Width" To="0" Duration="0:0:.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Label.Triggers>
</Label>
But since I use this trigger in multiple labels I want to write it in a style once.
This is my code to define controls:
<Border Name="Menu" Grid.Column="2" Grid.Row="1" Width="0" HorizontalAlignment="Right" BorderBrush="LightBlue" BorderThickness="2" CornerRadius="2" Background="LightCyan">
<StackPanel Name="MenuPanel">
<Button Style="{StaticResource MenuButtonsStyle}">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ListsMenu" Storyboard.TargetProperty="Height" To="86" Duration="0:0:.6"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<StackPanel Name="ListsMenu" Height="0">
<Label Name="Owners" Margin="0,1,0,0" MouseLeftButtonDown="SubMenuClicked" Style="{StaticResource SubMenuStyle}"/>
<Label Name="Contacts" MouseLeftButtonDown="SubMenuClicked" Style="{StaticResource SubMenuStyle}"/>
<Label Name="Groups" MouseLeftButtonDown="SubMenuClicked" Style="{StaticResource SubMenuStyle}"/>
</StackPanel>
</StackPanel>
</Border>
Scenario: There is a border, It's default width is zero, It will raise to 165 in another trigger which works fine, I want to animate it to zero again when labels are clicked, but I can not access the width of that border in lables style
You claim that you use triggers in multiple labels so you can define trigger one time like this
<Storyboard x:Key="animation">
<DoubleAnimation Storyboard.TargetName="ListsMenu" Storyboard.TargetProperty="Height" To="86" Duration="0:0:.6"/>
</Storyboard>
and later when you need you can call it
<EventTrigger RoutedEvent="MouseEnter" >
<BeginStoryboard Storyboard="{StaticResource animation}"/>
</EventTrigger>
But I'm a bit confused what you meant to acquire.
You cannot get access in style since style does not have a namescope, as it was said. If it changes label's width you could do this in style like this
<Style x:Key="style_button" TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width" To="300" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
but it invokes by StoryBoard.TargetName to another object.
try :
<EventTrigger RoutedEvent="MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.Target="{Binding ElementName=Menu}"
Storyboard.TargetProperty="Width" To="0" Duration="0:0:.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
TargetName is only valid inside Templates.

WPF XAML Grid Visibility Trigger

I have a status message located on the first row of my grid and I want it to slide in and out when the visibility changes.
The first visibility trigger works great and slides the first grid row open quickly. As soon as I add the 'Collapsed' trigger, nothing works at all. How do I reverse the animation to slide closed when the visibility is set to collapsed?
<Grid Grid.Row="0" Height="55" Visibility="{Binding StatusMessageVisibility, Mode=TwoWay}">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="0" To="55" Duration="0:0:.1" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="Visibility" Value="Collapsed">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="55" To="0" Duration="0:0:.1" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBlock Text="Hi There" />
</Grid>
You should remove the Visibility binding in your grid and use a DataTrigger that binds to the StatusMessageVisibility property. If you bind the grid's visibility then once it's collapsed it's collapsed and you won't be able to see the animation.
Also, instead of having two data triggers with EnterActions, use a single data trigger that also has an ExitAction for the collapsed state:
<Grid Grid.Row="0" Height="55">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding StatusMessageVisibility}" Value="Visible">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="0" To="55" Duration="0:0:.1" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="55" To="0" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBlock Text="Hi There" />
</Grid>

Don't trigger WPF animation if bound value is DateTime.MinValue

I have the following XAML code which triggers an animation of the rectangle when the LastDataUpdate field is changed. LastDataUpdate is a DateTime within a class that implements INotifyPropertyChanged. I'd like the animation to NOT run if the LastDataUpdate==DateTime.MinValue. Is there any way I can implement this in the XAML?
<Rectangle x:Name="NewDataAnimation" Tag="{Binding Path=LastDataUpdate, NotifyOnTargetUpdated=True}" Opacity="0" Width="5" Height="5" Fill="LawnGreen" HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle.Style>
<Style>
<Style.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Duration="0:0:0" To="1.0" />
<DoubleAnimation Storyboard.TargetProperty="Opacity" Duration="0:0:4" From="1.0" To="0.0" BeginTime="0:0:2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
Andrew
Why, not trigger on the property itself and have a converter determine whether to execute animation?
Below XAML assumes that you create a converter which will evaluate the LastDataUpdate against the value that you don't want. If you set LastDataUpdate to DateTime.MinValue, the animation won't start. As soon as you make a change, the binding will be forced to re-evaluate through PropertyChanged, and will let converter do its decision-making...
<Rectangle Opacity="0" Width="5" Height="5" Fill="LawnGreen" HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle.Style>
<Style>
<Style.Triggers>
<Trigger Property="{Binding LastDataUpdate, Converter={StaticResource LastDataUpdateToDoAnimation}}" Value="True" >
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" Duration="0:0:0" To="1.0" />
<DoubleAnimation Storyboard.TargetProperty="Opacity" Duration="0:0:4" From="1.0" To="0.0" BeginTime="0:0:2" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>

Smoothly resize an UIElement

I have a set of Ellipses on my Canvas.
For the MouseEnter event on each of the ellipses, I would like to resize the element so as to give a magnifying look and feel.
To make it more attractive, I want to make the change gradual (smooth/animated feeling). Any hints are appreciated.
Try something like this:
<Style x:Key="ScaleStyle" TargetType="{x:Type FrameworkElement}">
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform />
</Setter.Value>
</Setter>
<Style.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="1.2" Duration="0:0:0.2"
Storyboard.TargetProperty="RenderTransform.ScaleX" />
<DoubleAnimation To="1.2" Duration="0:0:0.2"
Storyboard.TargetProperty="RenderTransform.ScaleY" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="1.0" Duration="0:0:0.1"
Storyboard.TargetProperty="RenderTransform.ScaleX" />
<DoubleAnimation To="1.0" Duration="0:0:0.1"
Storyboard.TargetProperty="RenderTransform.ScaleY" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Canvas>
<Ellipse Style="{StaticResource ScaleStyle}" Canvas.Left="100" Canvas.Top="100"
Width="200" Height="100" Stroke="Black" StrokeThickness="2" Fill="Transparent" />
</Canvas>

WPF DoubleAnimation does not decrease the value after an animation

Consider the following simple WPF form, we will try to animate border1's Height:
This is the XAML for border1:
<Border Margin="3" BorderBrush="Black" BorderThickness="1" Name="border1">
<Border.Style>
<Style>
<Setter Property="Control.Height" Value="50" />
<Style.Triggers>
<DataTrigger Binding="{Binding X}" Value="1">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Height" To="100" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding X}" Value="2">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Height" To="200" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding X}" Value="3">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Height" To="300" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding X}" />
</Border>
X is a normal DependencyProperty and buttons do their work without problem and TextBlock's content inside border1 shows the changed value.
Now the problem is Height can be only Increased! For example by pressing 1, Height increases to 100, pressing 3 increases it to 300 but pressing 2 does not change the height.
If I set the initial border1's height to, for example, 400, all buttons can decrease it to 100, 200 or 300 but after this stage no animation can decrease border's height.
Am I missing some obvious point regarding WPF animation?
Update
Part of the issue is the storyboard in the DataTrigger is not being stopped, so (the last DataTrigger defined) is always holding the Height's 'animated' value.
Adding a StopStoryboard in the exit action fixes part of it. You can now switch between all three heights again... but the animation always starts from the Height's base value (which is clearly not what you want). You want it to start from where it was last set at.
The following MSDN article explains the second issue pretty well:
http://msdn.microsoft.com/en-us/library/aa970493.aspx
Good Solution- EventTriggers on your three buttons seem to work better and doesn't have the problems that the DataTrigger encounters (plus you don't even need the dependency property 'X' anymore!)
<Border Margin="3" BorderBrush="Black" BorderThickness="1" Name="border1">
<Border.Style>
<Style>
<Setter Property="Control.Height" Value="50" />
</Style>
</Border.Style>
<TextBlock Text="{Binding X}" />
</Border>
and
<StackPanel Orientation="Horizontal">
<Button Content="1" Width="50">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetName="border1" Storyboard.TargetProperty="Height" To="100"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<Button Content="2" Width="50">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetName="border1" Storyboard.TargetProperty="Height" To="200"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<Button Content="3" Width="50">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetName="border1" Storyboard.TargetProperty="Height" To="300"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</StackPanel>
There must be a simpler way to do this, but I cannot recall if there is at the moment.
Otherwise this will work. You can set the ExitAction of your triggers to set back to the default value. Do this by using a DoubleAnimation with no To value.
i.e.,
<DataTrigger Binding="{Binding X}" Value="1">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" Duration="0:0:0.2" To="100" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>

Resources