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
Related
I'm getting an error with the following details:
Source Name property cannot be set within Style. Triggers section
<Rectangle Margin="121,163,0,248" HorizontalAlignment="Left" Width="33" Height="34">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="Blue"></Setter>
<Style.Triggers>
<EventTrigger SourceName="myButton" RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Fill.Color" To="Orange"
Duration="0:0:1" AutoReverse="True" RepeatBehavior="Forever"
BeginTime="0:0:0">
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
I want to change the rectangle fill color with Color Animation tag when click on button.
Like it says, you cannot use source name in a style like that.
You can use a data trigger instead. Set say a bool property in your viewmodel from your button's command of click.
Then start your storyboard with a datatrigger binding that bool property and comparing value.
You can probably easily Google datatrigger and storyboard but here's a so question includes an example.
WPF Data Triggers and Story Boards
Btw.
Routed events are rarely very useful IME. Binding icommand is way more practical. Usually.
Edit:
Here's a quick and dirty sample using a togglebutton. Since this approach uses binding it can reference controls by name. Binding is resolved at run time.
<Grid>
<Rectangle>
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="Blue"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=StartStop}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="ColourStoryboard">
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Fill.Color" To="Orange"
Duration="0:0:1" AutoReverse="True" RepeatBehavior="Forever"
>
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="ColourStoryboard"/>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<ToggleButton Content="Start Stop"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Name="StartStop"/>
</Grid>
</Window>
Paste the grid inside a mainwindow, spin it up. When you click the togglebutton it sets ischecked true so the rectangle animates to orange and back to blue. When you click the button again, ischecked becomes false and the animation stops.
You could instead write code in a button handler that set a bound property which is in the datacontext and bind the datatrigger to that bound property. That's what the markup in the link is doing with IsBusy.
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>
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.
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.
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>