WPF TextBlock animation not firing when source is updated - wpf

I am simply trying to have some text flash red momentarily when the source for this TextBlock is updated. The text that the TextBlock is bound to works just fine, but for some reason the animation won't fire. I'm at a bit of a loss.
Any ideas?
<Border BorderBrush="{StaticResource Button.BackgroundBrush}"
Background="{StaticResource Screener.Background}"
BorderThickness="0,1,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TextBlock Text="{Binding AlertBoxMessage, Mode=OneWay, NotifyOnSourceUpdated=True}"
Name="AlertBox"
MinHeight="55"
FontWeight="Normal"
FontSize="16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Black">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<EventTrigger RoutedEvent="Binding.SourceUpdated" >
<BeginStoryboard>
<Storyboard Storyboard.TargetProperty="Foreground">
<ColorAnimation From="Black"
To="Red"
AutoReverse="True"
RepeatBehavior="3"
Duration="0:0:2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>

First of all I think you misinterpreted the Binding.SourceUpdated event.
Each binding consists of two endpoints - a source and a target. The target is the object on which the binding is set, target property is the DependencyProperty whose value will be bound, the source is the object against which Binding.Path will be resolved, and source property is the property to which the target property is bound. In your case the target is the TextBlock, target property is Text, source is the (inherited) data context (a view-model I presume), and source property is AlertBoxMessage.
As you probably know bindings can work in several modes. TwoWay will allow updating in both source-to-target and target-to-source directions, whereas OneWay will only do source-to-target updates.
Key information here is that the Binding.SourceUpdated will be raised whenever a target-to-source transfer occurs (and Binding.TargetUpdated for source-to-target transfer, see MSDN). In your case however the value is always updated in source-to-target direction, because you set the binding mode to OneWay, so Binding.SourceUpdated is never raised. To achieve your goal you should use Binding.TargetUpdated event instead (with Binding.NotifyOnTargetUpdated set to true).
There are however several other issues with your code that will prevent it from working, so let's go over them:
You're trying to animate the TextBlock.Foreground property (of type Brush) with a ColorAnimation, which can only be used to animate a property of type Color, and you'll get an InvalidOperationException. You should set StoryBoard.TargetProperty="Foreground.Color"
It may seem a bit counter-intuitive but setting RepeatBehavior="3" will make the animation repeat for 3 days, and not 3 times (see this question). You should use RepeatBehavior="3x"
To sum things up, here's the code that should do what you expect:
<TextBlock Text="{Binding AlertBoxMessage, Mode=OneWay, NotifyOnTargetUpdated=True}"
Name="AlertBox"
MinHeight="55"
FontWeight="Normal"
FontSize="16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Black">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard Storyboard.TargetProperty="Foreground.Color">
<ColorAnimation From="Black"
To="Red"
AutoReverse="True"
RepeatBehavior="3x"
Duration="0:0:2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>

Related

WPF ItemsControl DataTrigger EnterActions Animation not starting

I have a problem with an animation within a template of an ItemsControl that is part of the template of another ItemsControl. I want a path, that is representing an Icon, to change its color and constantly rotate when a certain condition becomes true.
The DataTrigger generally works, causing the Fill of the Path changing from Gray to LightGreen when the producing Property changes to true. However, the animation does not start. When I let the animation start with the Loaded Event (as you can see in the commented section), it starts properly. So I know that the animation, as well as the DataTrigger, is configured correctly.
When I put the same Path (just copy and paste) in the outside ItemsControl and change the DataTrigger to a Property of the corresponding DataType, the animation also works as expected. So there seems to be a problem with the nested ItemsControls, but I have no idea what it might be.
<ItemsControl ItemsSource="{Binding Computers}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type models:ClientComputerWrapper}">
<Border Margin="5" BorderBrush="Black" BorderThickness="1" Padding="5">
<StackPanel>
<!-- Some Content -->
<ItemsControl ItemsSource="{Binding PlcReaderStatuses}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type resources:PlcReaderStatusResource}">
<DockPanel LastChildFill="False">
<!-- Some Content -->
<Path DockPanel.Dock="Left" HorizontalAlignment="Left" Data="{StaticResource GearIconGeometry}" Stretch="Uniform" RenderTransformOrigin="0.5, 0.5">
<Path.RenderTransform>
<RotateTransform x:Name="gearPathTransform"/>
</Path.RenderTransform>
<Path.Style>
<Style TargetType="Path">
<Setter Property="Fill" Value="Gray"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsProducing}" Value="True">
<Setter Property="Fill" Value="LightGreen"/>
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="rotateStoryBoard">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Path.RenderTransform).(RotateTransform.Angle)" To="360" Duration="0:0:2" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<RemoveStoryboard BeginStoryboardName="rotateStoryBoard"/>
</DataTrigger.ExitActions>
</DataTrigger>
<EventTrigger RoutedEvent="Loaded">
<!--<BeginStoryboard x:Name="rotateStoryBoard">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Path.RenderTransform).(RotateTransform.Angle)" To="360" Duration="0:0:2" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>-->
</EventTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I've finally found a solution. After trying to replace the DoubleAnimation with a ColorAnimation, using (Path.Fill).(SolidColorBrush.Color) as TargetProperty, I received an error that the path is not pointing to a DependencyProperty. This was only solved when I defined the SolidColorBrush explicitly in a Setter which led my to try the same with the RenderTransform of the Path:
<Path.Style>
<Style TargetType="Path">
<Setter Property="RotateTransform">
<Setter.Value>
<RotateTransform x:Name="gearPathTransform"/>
</Setter.Value>
</Setter>
<!-- ... -->
</Style>
Afterwards the animation works as expected. I still don't know why this is only necessary in the inner ItemsControl and it is also nasty that there is no error as is for the ColorAnimation, but at least it is working now.

WPF Visibility animation trigger problems

I have two stackpanels, where the second panel is extra information that can be slided down and shown when clicking on a button (like jQuerys slideDown effect). And afterwards be slided up, when clicking the button again.
I´ve never been fiddling with animations before, but have been doing some research. I´m still quite confused though, and cant figure out this simple problem.
When I only listen on the Visibility=Visible property, it works fine. But when I also want to slide the panel up, it behaves weird.
This is my XAML code:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Width="600" Orientation="Horizontal">
<TextBlock Style="{StaticResource Heading4}">Panel 1</TextBlock>
<Button Width="300" Margin="30,0,0,0" Click="Button_OnClick">Click to slide other panel down</Button>
</StackPanel>
<StackPanel Name="StackPanelShowHide" Grid.Row="1" Width="500" Orientation="Vertical" Background="Beige" Height="70">
<StackPanel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="0" To="70" Duration="0:0:1" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="Visibility" Value="Collapsed">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="70" To="0" Duration="0:0:1" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Style="{StaticResource Heading4}">New panel</TextBlock>
</StackPanel>
</Grid>
And this is my Codebehind:
private void Button_OnClick(object Sender, RoutedEventArgs E) {
if (StackPanelShowHide.Visibility == Visibility.Collapsed) {
StackPanelShowHide.Visibility = Visibility.Visible;
} else {
StackPanelShowHide.Visibility = Visibility.Collapsed;
}
}
Really hope you can help :)
Kind regards,
Lars
I guess that instead of slide up animation on collapse it just instantly disappears. This is because you set Visibility to Collapsed, so there is nothing to display.
To fix this:
a) use MVVM: add some property to ViewModel and trigger on it. Modify property via ICommand.
b) Do not set Visibility, just start proper Storyboard in event handler.
I believe that your problem is that when the StackPanel has a Visibility value of Collapsed, it is removed from the UI. Therefore, even if the Animation were to occur, you would not see it.

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.

Can't get (fairly simple) WPF animation working

I'm trying to get a message panel animated in WPF but has so far achieved no success.
This is the situation:
I have a user control with a StackPanel containing an ItemsControl bound to an (observable) collection in the control's View Model object (ViewModel.Messages).
When I need to present the user with messages I ad those (as MessageVM instances) to the observable collection.
The ItemsControl's visibility is bound to an integer property called ViewModel.CountVisibleMessages and there's a converter taking care of translating 0 to Visibility.Hidden and positive values to Visibility.Visible.
This works just fine. When a message gets added to the collection the StackPanel automatically becomes visible and as the user (or a timer) removes the last message it gets hidden. The StackPanel height is automatically adjusted to fit all messages of course.
To make everything look nicer I would prefer it if the StackPanel resized itself using an animation running for, say, 300 ms. (Ultimately I would also like it to accelerate and deccelerate but that's beyond my ambition right now.
I have experimented for a few hours now but I feel I'm not even close.
Below is my current (not even close to working) XAML at the moment:
<StackPanel Orientation="Vertical"
VerticalAlignment="Top"
Visibility="{Binding CountVisibleMessages, Converter={StaticResource IntToVisibility}}"
Height="Auto"
Background="{DynamicResource HmiBackColorLightBrush}">
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CountVisibleMessagesChanged}" Value="True" ><!-- I suppose I shopuld've used a Routed Event here but I just needed to get it triggered -->
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Margin.Bottom"
From="100" <!-- Just a made up value to test the concept -->
To="0"
Duration="0:0:0:3"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<ItemsControl ItemsSource="{Binding Messages}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{DynamicResource Message}">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="15" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Text}" Margin="3" Style="{Binding MessageType, Converter={StaticResource MessageTypeToStyle}, ConverterParameter={x:Type TextBlock}}" /> <!-- using dynamic styling here -->
<RadioButton Grid.Column="1" Style="{DynamicResource HmiCloseMessageButton}" IsChecked="{Binding IsVisible, Converter={StaticResource BoolToVisibility}, ConverterParameter=true}" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
(I do realize the above XAML won't get the StackPanel to auto-resize slowly. It's just an experiment to get anything happening).
This can't be too difficult I suppose (it's a pretty standard UI behavior in many programs) so I'd appreciate it if anyone could point me in the right directions.
Cheers
Are you sure you want to adjust the bottom margin? The stack panel VerticalAlignment is top. If you want to change the Height then bind your StoryBoard Property to Height. Do you know if your StoryBoard is firing?
The key point is ExitActions are necessary for EnterAction based dataTrigger animations. So the following seems to be working in my case ....
<StackPanel Grid.Row="0" Height="100" x:Name="MyGrid">
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding ElementName=MyCheckBox,
Path=IsChecked}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.Target="{Binding
RelativeSource={RelativeSource
AncestorType={x:Type
StackPanel}},
BindsDirectlyToSource=True}"
Storyboard.TargetProperty="Height"
From="100"
To="200"
Duration="0:0:1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.Target="{Binding
RelativeSource={RelativeSource
AncestorType={x:Type
StackPanel}},
BindsDirectlyToSource=True}"
Storyboard.TargetProperty="Height"
To="100"
Duration="0:0:1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
</StackPanel>
<CheckBox x:Name="MyCheckBox" Grid.Row="1" />
Let me know if this helps.

Resources