EventTrigger Not Working When Declared in Window.Resources in WPF - wpf

I am new to WPF, so I may be missing something essential, but I have experimented and tried to come up with an explanation for the following phenomenon, to no avail.
Basically, the following code works (displays animation):
<Window.Resources>
<Storyboard x:Key="LoadStoryBoard"
AutoReverse="True"
RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="button1"
Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
...
<Button x:Name="button1" Grid.Column="0" Grid.Row="1" Style="{StaticResource Load}">
<Button.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</Button.Triggers>
</Button>
However, when I try to put the eventrigger in the Load Style in the following, the animation ceases to appear:
<Window.Resources>
<Storyboard x:Key="LoadStoryBoard"
AutoReverse="True"
RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="button1"
Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
...
<Style x:Key="Load" TargetType="Button">
...
<Style.Triggers>
...
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</Style.Triggers>
</Style>

In the Style of triggers can not use objects with TargetName, such animation. To do this, they are placed in triggers template <ControlTemplate.Triggers>. Quote from link:
TargetName is not intended for use within the Triggers collection of a Style. A style does not have a namescope, so it does not make sense to refer to elements by name there. But a template (either DataTemplate or ControlTemplate) does have a namescope.
The following works:
<Window.Resources>
<Storyboard x:Key="LoadStoryBoard" AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="button1" Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Style x:Key="ButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Green" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="button1" CornerRadius="0" Background="{TemplateBinding Background}">
<Grid>
<ContentPresenter x:Name="MyContentPresenter" Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,0" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Orange" />
</Trigger>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Button Name="TestButton" Style="{StaticResource ButtonStyle}" Width="100" Height="30" Content="Test" Grid.Column="0" Grid.Row="1" />
</Grid>
Notice that now TargetName in the template specified in the Border: <Border x:Name="button1" .../>.
Note: Or, you can just remove the Storyboard.TargetName, since it triggers the style is not supported.

You are correct that the EventTrigger is not working, but it is not because it was declared in the Resources section. To see this, you can move your style directly into the Button declaration where it still does not work:
<Button x:Name="button1" Grid.Column="0" Grid.Row="1">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
However, if we move the declaration of the Animation from the Resources section, it works again:
<Button x:Name="button1" Grid.Column="0" Grid.Row="1">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
So it seems as though the problem has something to do with the Storyboard declared in the Resources section not being ready by the time the Loaded event fires. There is a similar problem noted in this post.
However, just to confuse things more, if we then put the full declaration for the Animation into the Style declared in the Resources section, then now the Style works:
<Window.Resources>
<Style x:Key="Load" TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Button x:Name="button1" Grid.Column="0" Grid.Row="1" Style="{StaticResource Load}" />
I could speculate as to why this happens, but I'm guessing that there are very few WPF developers that really know why everything is the way that it is... I've learnt that if a particular declaration method works, use it and if not, try a different one.
Background
In WPF, there are four places where we can define Triggers; Style.Triggers, ControlTemplate.Triggers, DataTemplate.Triggers and FrameworkElement.Triggers (eg. Button.Triggers).
Basically, there is a huge flaw in the FrameworkElement.Triggers TriggerCollection in that it only accepts triggers of type EventTrigger. This can be seen on the FrameworkElement.Triggers Property page at MSDN where the following definition is given as to what this property can accept:
One or more defined EventTrigger elements. Each such trigger is
expected to contain valid storyboard actions and references. Note that
this collection can only be established on the root element of a page.
The MSDN property pages for the other trigger properties each announce that they can accept either Zero or more TriggerBase objects, or One or more TriggerBase objects.
Furthermore, there are distinct rules that different triggers follow - a unified approach would have certainly helped newcomers to WPF. From the FrameworkElement.Triggers Property page:
This property does not enable you to examine triggers that exist as
part of styles in use on this element. It only reports the collection
of triggers that are literally added to the collection, either in
markup or code. Elements do not typically have such elements existing
by default (through a template for instance); it is more common for
triggers that come from control compositing to be established in
styles instead.
In terms of behavior (and trying to establish which effect came from
which element's declared Triggers collection), both the triggering
condition and the trigger effect might be on this element, or might be
on its child elements in the logical tree. Note that if you use
lifetime events such as Loaded to get this collection, the child
element's triggers might not yet be fully loaded, and the collection
will be smaller than it would truly be at run time.
Note that the collection of triggers established on an element only
supports EventTrigger, not property triggers (Trigger). If you require
property triggers, you must place these within a style or template and
then assign that style or template to the element either directly
through the Style property, or indirectly through an implicit style
reference.
From the DataTemplate.Triggers Property page at MSDN:
If you are creating triggers within a data template, the setters of
the triggers should be setting properties that are within the scope of
the data template. Otherwise, it may be more suitable to create
triggers using a style that targets the type that contains the data.
For example, if you are binding a ListBox control, the containers are
ListBoxItem objects. If you are using triggers to set properties that
are not within the scope of the DataTemplate, then it may be more
suitable to create a ListBoxItem style and create triggers within that
style.
Unfortunately, all this extra information doesn't actually answer your question as to why the animation resource does not work in the Style resource, but hopefully now, you can see that the whole Trigger area is a bit of a complicated, messy area. Not being an expert myself, I just tend to use whichever method of declaring Triggers that works.
I hope that helps in some way.

Related

How to implement a shrink animation for a control in WPF

I want a control (e.g. a GroupBox) to show a grow animation when it becomes visible and a shrink animation, when the visibility is changed to "Collapsed".
Therefore, I created a style which implements an animated grow and shrink effect as shown here in a small sample application (shown below).
However, only the grow animation is shown. Instead of showing the shrink animation, the groupbox disappears at once.
Can anyone tell me, why?
And even better, how to fix it?
<Window x:Class="ShrinkTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="FrameworkElement" x:Key="ExpandableElement">
<Setter Property="RenderTransformOrigin" Value="0.5 0" />
<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" From="0" To="1" Duration="0:0:0.5" AccelerationRatio="0.2" DecelerationRatio="0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="Visibility" Value="Hidden">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" From="1" To="0" Duration="0:0:0.5" AccelerationRatio="0.2" DecelerationRatio="0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Margin="8" Width="140" Click="ButtonBase_OnClick">Expand / Shrink</Button>
<TextBlock Grid.Row="1" Text="--- Header ---"/>
<GroupBox x:Name="GroupBox" Grid.Row="2" Header="GroupBox" Style="{StaticResource ExpandableElement}" >
<StackPanel Orientation="Vertical">
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
</StackPanel>
</GroupBox>
<TextBlock Grid.Row="3" Text="--- Footer ---"/>
</Grid>
</Window>
I had a similar problem. Just think about it for a minute... your problem is that you can see your animation when it's visible, but you can't when it is hidden. That is also your answer to why... because it is hidden. I know, that's fairly unsatisfactory answer, but that's just how it is.
As to how to fix it... well saying it is simple, but implementing it is not. Simply put, you have to run your animation until it ends and then set the Visibility to Hidden. So unfortunately this means that nice, simple setting the Visibility property in the Trigger is no longer viable... it's ok to make it visible, just not for hiding.
In my case, I have a whole framework that I built my animations into. Basically speaking though, when I remove items from the collections, internally the item is not actually removed, but instead its exit animation is started. Only when that animation is complete will the internal collection actually remove the item.
So if you can be bothered, then you'll have to implement something like this where, rather than setting the Visibility property to Hidden, you set another property to true which triggers the animation and when the Completed event from that animation is called, then you set the Visibility property to Hidden.
Sheridan is right. As soon as a control becomes invisible, it doesn't matter, which animation you apply to it. :-)
So I created a special ExpandingContentControl:
public class ExpandingContentControl : ContentControl
{
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
"IsExpanded", typeof(bool), typeof(ExpandingContentControl), new PropertyMetadata(false));
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
public ExpandingContentControl()
{
Visibility = IsExpanded ? Visibility.Visible : Visibility.Collapsed;
}
}
But there was also a problem with the style: Creating two triggers which are bound to different values of the same property obviously doesn't work.
Instead, I'm now using just one trigger where the EnterAction implements growing and the ExitAction implements shrinking the control:
<Style TargetType="controls:ExpandingContentControl" >
<Setter Property="RenderTransformOrigin" Value="0.5 1" />
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform/>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" >
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" KeyTime="00:00:00"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" From="0" To="1"
Duration="0:0:0.3" DecelerationRatio="0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" From="1" To="0" Duration="0:0:0.2" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" KeyTime="00:00:0.2"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>

WPF DataTemplate: Location of inflated storyboard

I have a DataTemplate for an ItemsControl which is working fine. There is a DataTrigger in the DataTemplate which contains a BeginStoryboard EnterAction. I am trying to wire up the Completed event of the storyboard to something in code behind, specifically a method on the data object, but I can be flexible about that - at the moment I just want it to run any piece of C# code when the animation has completed.
Specifying a value for the Completed XAML attribute does not compile as the attribute is defined inside a template so there is no specific method to wire up to. So I will need to use code behind to wire up the event manually.
To this end I have looked at the application with Snoop to try to find where in the logical or visual tree the inflated template Storyboards end up. So far all I can see is a ContentControl created for each item, with its ContentTemplate set. The Content property of each ContentControl is set to its corresponding data object. The ContentTemplate property contains the Triggers collection which contain the EnterActions and ultimately the Storyboard. My question is, do all the items share a single template instance for their ContentTemplate property, or do they each get their own copy? If they share one, then where are the inflated triggers and storyboards created?
I've extracted the pertinent parts of my XAML:
<Style TargetType="{x:Type m:MyControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type m:MyControl}">
<Grid Name="ControlRoot" DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<!-- ... -->
<ItemsControl ItemsSource="...">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type m:MyDataType}">
<Grid>
<Ellipse Name="IconHighlight1" Fill="{DynamicResource GoldRadialFade}" Width="70" Height="70" StrokeThickness="0" Opacity="0"/>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Highlighted}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard HandoffBehavior="Compose">
<Storyboard Name="ConnectToMe" Duration="0:0:2.5" FillBehavior="Stop">
<DoubleAnimation To="400" Duration="0:0:1.5" Storyboard.TargetName="IconHighlight1" Storyboard.TargetProperty="Height" FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In such cases, I'd normally prefer to have a bool in the DataContext of the Item that your Storyboard is applying to and say call it AnimationCompleted
Now by modifying your Storyboard to
<Storyboard x:Key="ConnectToMe" Duration="0:0:2.5" FillBehavior="Stop">
<DoubleAnimation To="400" Duration="0:0:1.5" Storyboard.TargetName="IconHighlight1" Storyboard.TargetProperty="Height" FillBehavior="Stop" />
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="DataContext.AnimationCompleted" FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame Value="False" KeyTime="0:0:0" />
</BooleanAnimationUsingKeyFrames>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="DataContext.AnimationCompleted" FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame Value="True" KeyTime="0:0:2.5" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
We toggle the bool AnimationCompleted to true at the end point of the animation. Hence in the property setter of AnimationCompleted check if the incoming value is True and trigger your corresponding function/method from there

How to let control set its own property if event fires

How can I have a control's property to be set to a specific value, if a event of the same control fires?
Let's say I have an expander
<Expander Header="Click to expand" GotFocus="IsExpanded=True" />
And I want to set the IsExpanded Property to true, if it got Focus.
How can I do this in Xaml?
You can try to use binding, probably something like this:
<Expander IsExpanded="{Binding IsFocused, RelativeSource={RelativeSource Self}, Mode=OneWay}" />
Adrian's approach is the cleanest way to reach your goal. However, if you want to change a property when an event fires, you can try this:
<Expander Header="Click to expand">
<Expander.Style>
<Style TargetType="Expander">
<Style.Triggers>
<EventTrigger RoutedEvent="GotFocus">
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Expander.IsExpanded)">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Expander.Style>
</Expander>
Note: this is purely from memory, and may not work as-is. But it should give you a good idea of how this could be accomplished.

Time Delay on Trigger

I wish to attach a time delay to a mouseover event on a WPF expander I have on my form (xaml supported by VB.NET code behind). This mouseover event essentially triggers the expansion as oppose to clicking - but I'd like a short wait before the content is expanded. So far I have not managed to find anything to solve this via the wider internet.
The current xaml code to enable the trigger is:
<Style x:Key="HoverExpander" TargetType="{x:Type Expander}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsExpanded" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
This style is then applied to:
<Expander Style="{StaticResource HoverExpander}"
HorizontalAlignment="Right"
ExpandDirection="Left"
Height="Auto"
Width="Auto">
<!-- Content here -->
</Expander>
Note that I've stripped out other aesthetics (such as borders, gridrefs etc for readability).
I think there should be some way to set a delay on the MouseOver Trigger but haven't had much luck finding it. This could either be set in xaml or perhaps as an event in the code behind.
I'm working on this currently, so when I find a solution I shall post it here. Grateful for any ideas meantime. Thanks!
Use an EventTrigger on the MouseOver event and a Storyboard with a BooleanAnimationUsingKeyFrames instead. In the Timeline of the Storyboard, you could have KeyFrames, so that the animation waits for some time before it affects the properties you want to change.
This was the code I settled on - based on the ideas already given:
<Style x:Key="HoverExpander" TargetType="{x:Type Expander}">
<Style.Setters>
<Setter Property="IsExpanded" Value="False"/><!-- Initially collapsed -->
</Style.Setters>
<Style.Triggers>
<!-- Impose a short delay (500ms) before expanding control -->
<EventTrigger RoutedEvent="Expander.MouseEnter">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetProperty="IsExpanded"
Duration="0:0:0.5">
<DiscreteBooleanKeyFrame Value="True" KeyTime="100%"/><!-- I.E. after 500ms -->
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<!-- Collapse when mouse leaves control-->
<EventTrigger RoutedEvent="Expander.MouseLeave">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetProperty="IsExpanded"
Duration="0:0:0.1">
<DiscreteBooleanKeyFrame Value="False" KeyTime="0%"/><!-- I.E. Immediately -->
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
Then apply as before. This was tested and works in .NET 4.0. Other neat tricks could be applied if you do so wish, I found the following to be quite helpful in getting ideas:
Animation Overview (MSDN)
Storyboards Overview (MSDN)

WPF - Animating object lower/higher in visual tree

I have following problem.
I created Style for ContentControl that enables moving/dragging of specific item.
This is created with help of MoveControl (: Control) that controls mouse down/move/up events. In this class DependencyProperty IsDragging property is defined, that i want to use to fade in/out item when it changes state.
Xaml file for my syle looks something like this.
<Style x:Key="ItemStyle" TargetType="ContentControl">
<!-- ... -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl" x:Name="ctrl">
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=.}">
<s:MoveControl Cursor="SizeAll" Template="{StaticResource MoveThumbTemplate}" x:Name="moveThumb"/>
</Grid>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
<!-- ... -->
</Setter>
</Style>
So, i want to create animation that will be done on the ContentControl styled with ItemStyle when MoveControl.IsDragging will be set to true/false.
Thank you for help.
I figured out.
The solution was to use SourceName property and link it to object of which dependency property is used. The problem was that by default 'this' object references element's DataContext value.
If you set SourceName property to a non-null value, then the data binding operation will treat that value as the place where data is pushed to and pulled from
<ControlTemplate.Triggers>
<Trigger SourceName="moveThumb" Property="IsDragging" Value="true" >
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1.0" To="0.3" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>

Resources