Get a Framework element from a Storyboard - wpf

I have a reference to an instance of a Storyboard object, and want to get hold of the Framework element that it is attached to / animating. I haven't been able to come up with any way of doing this.
For example, in the XAML below, can I go from a reference to the Storyboard to get hold of either the Label or the Grid
<Grid>
<Grid.Resources>
<Storyboard x:Key="myStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:5"/>
</Storyboard>
<Style x:Key="myStyle" TargetType="{x:Type Label}">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=StartAnimation}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource myStoryboard}" />
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<Label x:Name="labelHello" Grid.Row="0" Style="{StaticResource myStyle}">Hello</Label>
</Grid>
For those wondering why on earth I need to do this, it is because I am trying to create a derived Storyboard class or an attached behaviour that will allow me to specify a method name on the DataContext to be called when the Storyboard completed event fires. This will allow me to do pure MVVM rather than needing some code behind to call into my View Model.

If you changed your XAML to something like this:
<Grid x:Name="grid">
<Grid.Resources>
<Storyboard x:Key="myStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:5" Storyboard.Target="{Binding ElementName = grid}"/>
</Storyboard>
<Style x:Key="myStyle" TargetType="{x:Type Label}">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=StartAnimation}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource myStoryboard}" />
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<Label x:Name="labelHello" Grid.Row="0" Style="{StaticResource myStyle}">Hello</Label>
</Grid>
This introduces an x:Name to the grid and a Storyboard.Target to the DoubleAnimation. You can now get a reference to the grid with this code:
Storyboard sb = //You mentioned you had a reference to this.
var timeLine = sb.Children.First();
var myGrid = Storyboard.GetTarget(timeLine);

Related

How to use style in xaml to move an ellipse on canvas

I'm practicing using xaml styles by making a simple traffic light controlled by radiobuttons. However, my code crashes because there is a reference error using Canvas.Top in the Ellipse style. Is there any way I could fix that while keeping everything in the style and not in the Ellipse itself?
Here is my 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"></Ellipse>
</Canvas>
</Window>
EDIT included setter for a Canvas.Top value and used parentheses for (Canvas.Top):
<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="82"></Ellipse>
</Canvas>
</Window>
Canvas.Top is an attached property so you should add parentheses around it:
Storyboard.TargetProperty="(Canvas.Top)"
You should also set the From property of the DoubleAnimation to a valid double such as for example 0:
<DoubleAnimation
Storyboard.TargetProperty="(Canvas.Top)" Duration="0:0:0.400" From="0" To="133">
Alternatively, you could set the Canvas.Top property of the element being animated to an initial value:
<Ellipse Name="Light" Width="50" Height="50" Canvas.Top="10" Canvas.Left="180"></Ellipse>
Or add a Setter to the Style:
<Setter Property="Canvas.Top" Value="10"/>
When you animate a property without setting the animation's From property, the animation starts from the current value of the property.
For Canvas.Top and the other Canvas attached properties, the default value (and hence the current value) is double.NaN, not 0.
Just set an initial value by a Style Setter:
<Style TargetType="Ellipse">
<Setter Property="Canvas.Top" Value="0"/>
...
</Style>
Or directly on the Ellipse:
<Ellipse Canvas.Top="0" .../>
You would also need to use the correct animated property path for attached properties, which uses parentheses:
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" .../>

The ellipse can't rotate in datatrigger

I want to rotate the ellipse when ProgressBar is set to true in DataTrigger. But It isn't work.
I test the ellipse is work OK with Opacity. But it isn't work with RotateTransform. And the ellipse is also work with EventTrigger.
Which is my mistake and how I fix it.
Thanks for reading my issue.
<Ellipse Name="ProgressEllipse" Width="40" Height="40" StrokeThickness="7" Stroke="{StaticResource ProcessEllipseGradient}" Margin="5,0,0,0">
<Ellipse.RenderTransform>
<RotateTransform CenterX="20" CenterY="20" Angle="0"/>
</Ellipse.RenderTransform>
<!--<Ellipse.Triggers>
<EventTrigger RoutedEvent="Ellipse.Loaded" SourceName="ProgressEllipse">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ProgressEllipse"
Storyboard.TargetProperty="(Ellipse.RenderTransform).(RotateTransform.Angle)"
From="0"
To="360"
Duration="0:0:1"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>-->
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=ProgressBar}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(Ellipse.RenderTransform).(RotateTransform.Angle)"
From="0"
To="1"
Duration="0:0:5"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
Addition information
Sorry WPF-it. I make my question not clear.
The Ellipse is work OK when It located in form directly. But It not work when located in a Template.
Follow is my code.
https://www.mediafire.com/?7rb8gvthx94rfiy
If I omit "RelativeSource={RelativeSource self}" in the Trigger. It will throw exception(Cannot animate '(0).(1)' on an immutable object instance)
In the sample code. I have three ellipse. The left ellipse is not work when I locate it in the template(I try to make it work, but It don't work). The center ellipse is the same code with the ellipse in template. But It work OK when I locate it directly in the form. And I test ellipse with Opacity property in the template. And It work OK. I don't know which is my mistake.
Can you give me a hand.
Thanks for reading my question
Ellipse.ProgressBar property does not exist in WPF. Your are probably referring some property from the underlying DataContext of Ellipse.
So change Binding="{Binding RelativeSource={RelativeSource Self}, Path=ProgressBar}" to simply Binding="{Binding Path=ProgressBar}"
Also make sure that this ProgressBar property generates property change notifications so that trigger will execute the action ...
Plus you should also have an EndStoryBoard also.
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=ProgressBar}"
Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Name="MyStoryboard">
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(Ellipse.RenderTransform).(RotateTransform.Angle)"
From="0"
To="1"
Duration="0:0:5"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="MyStoryboard"/>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>

WPF: Storyboard in style returning a "Cannot animate 'Color' on an immutable object instance."

I have the following XAML:
<UserControl.Resources>
<SolidColorBrush x:Key="Brush"></SolidColorBrush>
<Style TargetType="{x:Type StackPanel}" x:Key="ColourChangingStyle">
<Setter Property="Background" Value="{StaticResource Brush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path='Stage'}" Value="1">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.Target="{StaticResource Brush}"
Storyboard.TargetProperty="Color" From="{StaticResource FirstColor}" To="{StaticResource FinishedColor}" Duration="0:0:10" BeginTime="0:0:0"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel x:Name="InfoPanel" Orientation="Vertical" Margin="1,2" Style="{DynamicResource ColourChangingStyle}">
...
</StackPanel>
And I keep getting the same error that:
Cannot animate 'Color' on an immutable object instance
which is the brush. I looked up the problem, and believe that it's something to do with the binding the brush to the StackPanel making it unavailable to alter later.
Is there any way around this, I literally have no clue what my other options for the same effect are without hardcoding colors in, and doing all the events in code.
It seems that you can not animate a Brush in Resources with Storyboard.Target. But you can set the Background of your Style (you have done this already) and animate this property.
<ColorAnimation Storyboard.TargetProperty="Background.Color"
From="{StaticResource FirstColor}"
To="{StaticResource FinishedColor}"
Duration="0:0:10" BeginTime="0:0:0" />

How to animate ScaleY when Element becomes visible

I'd like to make a WPF UI Element appear to expand vertically when its Visibility property transitions to "Visible". I don't want to hard-code the Height in the animation since I'd like to apply this animation to any UI Element as a Style. So, I'm trying to use ScaleY but am not having any luck. Here is the XAML for the style and the listbox:
<Style x:Key="VerticalGrow" TargetType="ListBox">
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="TransformGroup.ScaleTransform.ScaleY" BeginTime="0:0:0.5" From="0" To="1" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
<ListBox Grid.Row="2" MaxHeight="60" MinHeight="60" Visibility="{Binding MyViewModel.ListBoxVisibility}" IsSynchronizedWithCurrentItem="False" ItemsSource="{Binding MyViewModel.ListBoxItems}" Style="{DynamicResource VerticalGrow}" IsTabStop="True">
</ListBox>
I get an runtime exception complaining that:
"Cannot convert the value in attribute 'Style' to object of type 'System.Windows.Style'. Cannot resolve all property references in the property path 'TransformGroup.RenderTransform.ScaleTransform.ScaleY'. Verify that applicable objects support the properties. Error at object 'System.Windows.Controls.ListBox' in markup file 'MyApp;component/mainwindow.xaml' Line 69 Position 399."}
ListBox doesn't have a property TransformGroup. I think you want to set RenderTransform or LayoutTransform to a ScaleTransform, and then animate that.
<Style x:Key="VerticalGrow" TargetType="ListBox">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform/>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="RenderTransform.ScaleY"
BeginTime="0:0:0.5" From="0" To="1" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>

WPF - Best way of responding to changes in a ViewModel at Page/Window level

I'm developing an XBAP and i have a simple requirement.
The DataContext of the whole main page is set to an instance of my UserViewModel. The UserViewModel has a DependencyProperty called AuthenticationState which is an enum with values like 'Authenticated','NotAutheticated' and 'AuthenticationFailed'.
Now, i need to respond to any change in this value by hiding/displaying various elements on the page.
What (and where) is the best way of doing that?
As you mentioned you can't use a DataTrigger directly on a control. A work around would be to use a style on each Control that needs to be hidden.
<Grid>
<Rectangle Fill="Red" />
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding Test}" Value="true">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
A preferable method would be to use a Converter called "AuthenticationStateToVisibilityConverter" that is used in binding the control's Visibility property to the data context's AuthenticationState property.
The best way would be to use a DataTrigger. So something like this:
<Window.Triggers>
<DataTrigger Binding="{Binding AuthenticationState}" Value="NotAuthenticated">
<Setter TargetName="nameOfControl" Property="Visibility" Value="Collapsed" />
</DataTrigger>
...
<TextBox x:Name="nameOfControl" />
</Window.Triggers>
As long as you UserViewModel object is in the DataContext of the Window then this should work!
Managed to sort it using styles. It's a pain but it works!
The full source is below.
<Grid x:Name="contentGrid" Grid.Row="1">
<!--login-->
<controls:LoginControl>
<controls:LoginControl.Style>
<Style>
<Setter Property="Control.Opacity" Value="0"/>
<Setter Property="Control.IsHitTestVisible" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Source={StaticResource UserViewModel},Path=UserAuthenticationState}"
Value="{x:Static model:AuthenticationState.NotAuthenticated}">
<Setter Property="Control.IsHitTestVisible" Value="True"/>
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="1" Duration="0:0:2"
Storyboard.TargetProperty="Opacity"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="0" Duration="0:0:2"
Storyboard.TargetProperty="Opacity"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</controls:LoginControl.Style>
</controls:LoginControl>
<!--slider-->
<slider:PageSlider>
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
<slider:PageSlider.Style>
<Style>
<Setter Property="Control.Opacity" Value="0"/>
<Setter Property="Control.IsHitTestVisible" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Source={StaticResource UserViewModel},Path=UserAuthenticationState}"
Value="{x:Static model:AuthenticationState.Authenticated}">
<Setter Property="Control.IsHitTestVisible" Value="True"/>
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="1" Duration="0:0:2"
Storyboard.TargetProperty="Opacity"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="0" Duration="0:0:2"
Storyboard.TargetProperty="Opacity"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</slider:PageSlider.Style>
</slider:PageSlider>
</Grid>
Actually, the best way to do this is to expose the appropriate properties from your view model. This makes your logic more centralized and easier to test. Also, it performs better than converters. It is, after all, a view model. Therefore, it should model the view. If the view needs a property to tell it when to hide / show a panel, add such a property to your view model.

Resources