Why is this WPF animation only triggered once? - wpf

I have a custom control with the following border definition:
<Border Width="10" Height="10"
CornerRadius="5"
Background="Red"
BorderBrush="White"
BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Opacity" Value="0.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MyState}" Value="{x:Static my:MyStates.Initializing}">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="Animate"/>
<BeginStoryboard x:Name="Animate" HandoffBehavior="SnapshotAndReplace">
<Storyboard Duration="0:0:0.4">
<DoubleAnimation AutoReverse="True" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.2" FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
The intention is: when MyState changes to a value of MyStates.Initializing, the border should fade in and out again.
I did not define <DataTrigger.ExitActions>, because MyState will be changed again very quickly; nothing should happen (except the animation finishing) when MyState is set to a different value.
The weird thing is: this only works once. If I have two instances of the same control: both will fire once, but never again. If I then add another instance, it will not fire either.
I searched through countless links and suggestions, e.g.:
WPF Animation Only Firing Once
https://social.msdn.microsoft.com/Forums/vstudio/en-US/7e074dc8-e5da-4840-8b54-8fcb67b43329/storyboard-run-only-one-time-inside-datatriggers?forum=wpf
Animation inside DataTrigger won't run a second time
DataTrigger and Storyboard only getting executed once, related to order of declaration?
What am I missing?
EDIT:
To clarify (after reading a comment below), the animation works for the first time it is triggered, but then never again. If MyState changes to Initializing, it is triggered (apparently) correctly. Then (before the animation is finished, a few milliseconds or less later) MyState changes to Whatever and the animation finishes as desired. If MyState then is again changed to Initializing (later, long after the animation has finished), nothing happens.
Also: If I have two instances that respond the their respective MyState changing to Initializing (within a few milliseconds or less), both are triggered correctly. I can then add a completely new instance and that will not be triggered.
What I also checked is, if a second instance would be triggered correctly, if the trigger (setting MyState == Initialized) came after the animation of the first instance has finished. Yes, every instance existing when the trigger first fires will be triggered correctly once. After that: nothing...
EDIT:
Below is the code I wrote to isolate and test the issue. In a previous edit I had assumed that the code was ok and my error must be hidden somewhere else, but I had tested with a sleep duration (see code below) of 1000ms. If I reduce that to 10ms, my problem persists. So, it appears to be a data binding issue - and not an animation issue.
AnimationTestControl.xaml.cs:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace AnimationTest
{
public enum MyStates
{
None = 0,
Initializing = 1,
Ready = 2,
}
public class TestItem : INotifyPropertyChanged
{
private MyStates _myState;
public MyStates MyState
{
get => _myState;
set { _myState = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public partial class AnimationTestControl : UserControl
{
public ObservableCollection<TestItem> TestItems { get; } = new ObservableCollection<TestItem>();
public AnimationTestControl()
{
InitializeComponent();
TestItems.Add(new TestItem());
}
private void ButtonStart_Click(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
foreach (var testItem in TestItems)
{
testItem.MyState = MyStates.Initializing;
Thread.Sleep(10); //1000ms = good, 10ms = bad
testItem.MyState = MyStates.Ready;
}
});
}
private void ButtonAdd_Click(object sender, RoutedEventArgs e)
{
TestItems.Add(new TestItem());
}
}
}
AnimationTestControl.xaml:
<UserControl x:Class="AnimationTest.AnimationTestControl"
x:Name="self"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:AnimationTest"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid DataContext="{Binding ElementName=self}">
<StackPanel Orientation="Vertical">
<ItemsControl ItemsSource="{Binding TestItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Border Width="10" Height="10"
CornerRadius="5"
Background="Red"
BorderBrush="White"
BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Opacity" Value="0.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MyState}" Value="{x:Static local:MyStates.Initializing}">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="Animate"/>
<BeginStoryboard x:Name="Animate" HandoffBehavior="SnapshotAndReplace">
<Storyboard Duration="0:0:0.4">
<DoubleAnimation AutoReverse="True" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.2" FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock Text="{Binding MyState}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal">
<Button Content="Start" Click="ButtonStart_Click" />
<Button Content="Add" Click="ButtonAdd_Click" />
</StackPanel>
</StackPanel>
</Grid>
</UserControl>

It turns out that the problem has nothing to do with the animation, but instead with the DataTrigger or WPF's throttling in general, see Is there a workaround for throttled WPF DataTrigger events?.
As the example code in the link above shows, the observed behavior (esp. regarding "first only" and additional instances) was not accurate or not as strictly reproducible as it seemed.

Related

In WPF, how to override an Event Trigger?

I have a StackPanel with multiple buttons. I want all the buttons except one to trigger an animation when the user clicks on them, so in the StackPanel.Triggers I have added this code:
<StackPanel.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource animationName}" />
</EventTrigger>
</StackPanel.Triggers>
In the particular button I have added this code:
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource anotherAnimation}" />
</EventTrigger>
</Button.Triggers>
When clicking the button, both animations begin, so it appears that the second EventTrigger is just added to the first one and not override it.
How can I override the first EventTrigger so only the second one will be triggered when clicking on that particular button?
Note: I need the answer to be in pure XAML without any code-behind involved.
EDIT: Here is the storyboard:
<Storyboard x:Key="animationName">
<DoubleAnimation
Storyboard.TargetName="PageFrame"
Storyboard.TargetProperty="Opacity"
To="0" Duration="0:0:0.25" />
</Storyboard>
Just use x:Key property for necessary buttons. For example:
<Window>
<Window.Resources>
<Style x:Key="myStyle" TargetType="Button">
<Setter Property="Background" Value="Green"/>
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource animationName}" />
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Button Style="{StaticResource myStyle}">Styles are cool!</Button>
<Button>No Animation:)</Button>
<Button Style="{StaticResource myStyle}">Yes to animation!</Button>
</StackPanel>
</Window>
Update:
If you want to avoid use Style just for a few buttons, just create Style for all Button controls and set Style="{x:Null}" to controls where you want to avoid animation. See the following example:
<Window>
<Window.Resources>
<!--This style will be applied to all Buttons, except where Style="{x:Null}"-->
<Style TargetType="Button">
<Style.Resources>
<Storyboard x:Key="animationName">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0" Duration="0:0:0.25" />
</Storyboard>
</Style.Resources>
<Setter Property="Background" Value="Green"/>
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource animationName}" />
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Button Content="Yes to Animation"/>
<Button Content="No Animation:)" Style="{x:Null}"/>
<Button Content="Yes to Animation"/>
</StackPanel>
</Window>
Update 1:
you have deleted the TargetName, but I really need to set it so the animation will be applied to the correct element.
Since a style can be reused in multiple places in a WPF application, we can't reference to a UIElement from within the style. This behavior is by design.
As promised I took #RayBurns answer from this link and modified it, to answer your question. The ConditionalEventTrigger is now looking like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Markup;
namespace Trigger
{
[ContentProperty("Actions")]
public class ConditionalEventTrigger : FrameworkContentElement
{
private static readonly RoutedEvent TriggerActionsEvent = EventManager.RegisterRoutedEvent("", RoutingStrategy.Direct, typeof(EventHandler), typeof(ConditionalEventTrigger));
public RoutedEvent RoutedEvent { get; set; }
public static readonly DependencyProperty ExcludedSourceNamesProperty = DependencyProperty.Register(
"ExcludedSourceNames", typeof (List<string>), typeof (ConditionalEventTrigger), new PropertyMetadata(new List<string>()));
public List<string> ExcludedSourceNames
{
get { return (List<string>) GetValue(ExcludedSourceNamesProperty); }
set { SetValue(ExcludedSourceNamesProperty, value); }
}
public static readonly DependencyProperty ActionsProperty = DependencyProperty.Register(
"Actions", typeof (List<TriggerAction>), typeof (ConditionalEventTrigger), new PropertyMetadata(new List<TriggerAction>()));
public List<TriggerAction> Actions
{
get { return (List<TriggerAction>) GetValue(ActionsProperty); }
set { SetValue(ActionsProperty, value); }
}
// "Triggers" attached property
public static ConditionalEventTriggerCollection GetTriggers(DependencyObject obj) { return (ConditionalEventTriggerCollection)obj.GetValue(TriggersProperty); }
public static void SetTriggers(DependencyObject obj, ConditionalEventTriggerCollection value) { obj.SetValue(TriggersProperty, value); }
public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached("Triggers", typeof(ConditionalEventTriggerCollection), typeof(ConditionalEventTrigger), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
// When "Triggers" is set, register handlers for each trigger in the list
var element = (FrameworkElement)obj;
var triggers = (List<ConditionalEventTrigger>)e.NewValue;
foreach (var trigger in triggers)
element.AddHandler(trigger.RoutedEvent, new RoutedEventHandler((obj2, e2) =>
trigger.OnRoutedEvent(element, e2)));
}
});
// When an event fires, check the condition and if it is true fire the actions
void OnRoutedEvent(FrameworkElement element, RoutedEventArgs args)
{
var originalSender = args.OriginalSource as FrameworkElement;
if(originalSender == null) return;
DataContext = element.DataContext; // Allow data binding to access element properties
if (!ExcludedSourceNames.Any(x=>x.Equals(originalSender.Name)))
{
// Construct an EventTrigger containing the actions, then trigger it
var dummyTrigger = new EventTrigger { RoutedEvent = TriggerActionsEvent };
foreach (var action in Actions)
dummyTrigger.Actions.Add(action);
element.Triggers.Add(dummyTrigger);
try
{
element.RaiseEvent(new RoutedEventArgs(TriggerActionsEvent));
}
finally
{
element.Triggers.Remove(dummyTrigger);
}
}
}
}
public class ConditionalEventTriggerCollection: List<ConditionalEventTrigger>{}
}
It can be used in your XAML like this. Take care that all SourceNames you don´t want to be recognized on execution of your actions are inside the ExcludedSourceNames section.:
<trigger:ConditionalEventTrigger.Triggers>
<trigger:ConditionalEventTriggerCollection>
<trigger:ConditionalEventTrigger RoutedEvent="Button.Click">
<trigger:ConditionalEventTrigger.ExcludedSourceNames>
<system:String>buttonTriggeringAnotherAnimation</system:String>
</trigger:ConditionalEventTrigger.ExcludedSourceNames>
<BeginStoryboard Storyboard="{StaticResource Storyboard1}"></BeginStoryboard>
</trigger:ConditionalEventTrigger>
</trigger:ConditionalEventTriggerCollection>
</trigger:ConditionalEventTrigger.Triggers>
To give you an ready to start example here is a window:
<Window x:Class="ConditionalEventTriggerExample.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:ConditionalEventTriggerExample"
xmlns:trigger="clr-namespace:Trigger;assembly=Trigger"
xmlns:system="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Storyboard x:Key="Storyboard1">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="rectangle">
<EasingColorKeyFrame KeyTime="0:0:1" Value="#FF5151FD"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Storyboard2">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="rectangle1">
<EasingColorKeyFrame KeyTime="0:0:1" Value="#FFFF7400"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
<StackPanel>
<StackPanel.Triggers>
<EventTrigger RoutedEvent="Button.Click" SourceName="buttonTriggeringAnotherAnimation">
<BeginStoryboard Storyboard="{StaticResource Storyboard2}"/>
</EventTrigger>
</StackPanel.Triggers>
<trigger:ConditionalEventTrigger.Triggers>
<trigger:ConditionalEventTriggerCollection>
<trigger:ConditionalEventTrigger RoutedEvent="Button.Click">
<trigger:ConditionalEventTrigger.ExcludedSourceNames>
<system:String>buttonTriggeringAnotherAnimation</system:String>
</trigger:ConditionalEventTrigger.ExcludedSourceNames>
<BeginStoryboard Storyboard="{StaticResource Storyboard1}"></BeginStoryboard>
</trigger:ConditionalEventTrigger>
</trigger:ConditionalEventTriggerCollection>
</trigger:ConditionalEventTrigger.Triggers>
<Button x:Name="button" Content="Button"/>
<Button x:Name="button1" Content="Button"/>
<Button x:Name="buttonTriggeringAnotherAnimation" Content="triggering another animation"/>
<Button x:Name="button3" Content="Button"/>
<Button x:Name="button4" Content="Button"/>
<Button x:Name="button5" Content="Button"/>
<Rectangle x:Name="rectangle" Fill="#FFF4F4F5" Height="100" Stroke="Black"/>
<Rectangle x:Name="rectangle1" Fill="#FFF4F4F5" Height="100" Stroke="Black"/>
</StackPanel>
If you don´t get it to work I can upload the solution on GitHub.

How to Animate a ContentControl in a ItemsControl

I like to use the ItemsControl to host ContentsControls. Each new ContentsControl is animating its contents when the item gets added and each ContentControl and overlays the previous one. The ItemsControl and the ContentControl Content is bound with Caliburn Micro using Naming conventions.
<ItemsControl x:Name="OverlayStackedItems" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="Transparent">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid x:Name="ItemsHost" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<cc:DummyContentControl cal:View.Model="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
The ContentControl is defined like this:
[ContentProperty("Content")]
public partial class DummyContentControl :ContentControl
{
public DummyContentControl()
{
}
static DummyContentControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DummyContentControl), new FrameworkPropertyMetadata(typeof(ContentControl)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
}
protected override void OnContentChanged(object oldContent, object newContent)
{
LayoutUpdated += (sender, e) =>
{
};
UpdateLayout();
base.OnContentChanged(oldContent, newContent);
}
void DummyContentControl_LayoutUpdated(object sender, EventArgs e)
{
throw new NotImplementedException();
}
protected override Size MeasureOverride(Size constraint)
{
return base.MeasureOverride(constraint);
}
}
So now finally my question. In the real ContentControl I like to animate the Content but
the ContentControl has the size of 0 when OnContentChange is called where my Animation gets created. The orders of calls when the ContentControl is hosted in the ItemsControl is:
OnContentChanged (Animation failes)
OnApplyTemplate
MeasureOverride
When the ContentControl runs by itself the order is:
OnApplyTemplate
MeasureOverride
OnContentChanged (Animation works)
The problem here is that the complete visual subtree of the new Item in the ItemsControl is 0 (DesiredSize,ActualSize = 0) therefore my animation code fails.
I hope that makes some sense to somebody,
Any help would be great, Thx,J
------------------------------Revision-------------------
Ok I added the OnLoaded eventhandler to the ctor of the DummyControl. The order of calles is
1. OnContentChanged (all sizes are 0)
2. OnApplyTemplate (all sizes are 0)
3. MeasureOverride (called several Times probably for all child controls hostet by the ContentControl)
4. Loaded event (Desired Size is set all other sizes are still 0)
Can sombody explain what the recommanded practice is on how to animate a ContentControl
hostet by an ItemsControl?
Just do everything in XAML and let the animation do it's thing, without calling MeasureOverride() and the rest of the hooks.
<ItemsControl>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border>
<TextBlock Text="Whatever your template should look like"/>
</Border>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard >
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)" Duration="0:0:0.5" From="0" To="1" />
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)" Duration="0:0:0.5" From="0" To="1" />
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.CenterX)" Duration="0:0:0.5" To="25" />
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.CenterY)" Duration="0:0:0.5" To="25" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>

Updating a wpf style having infinite possible conditions

I have a DataGrid with rows representing a host I'm doing pings to and a column called Lost which represents lost ICMP packets which over time increases in value. I have the whole INotifyPropertyChanged thing down and I'm seeing value increase. What I want to do is write a Style that'll change a row's background color from white to dark red progressively relative to the Lost column's value.
I would like, if it were possible, to write a Trigger or DataTrigger with a setter value set to a ValueConverter which would calculate the color needed, but so far I've been unsuccessful in writing a style that will update every time a Lost cell's value changes. I only see a difference in color when I load a new data context and switch back (just for testing).
Here's what I've tried:
<Style x:Key="DownStyle" TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="{Binding Converter={StaticResource BackgroundConverter}}" />
</Style>
I can't see this working with a DataTrigger since you have to specify a value anyways and the values are infinite (or Int32.MaxValue I guess), really. Although I also tried specifying a ValueConverter for the value property and that didn't work either.
Btw, I want to try to avoid code-behind if possible.
Edit:
Rick: I had tried doing something like:
<Style x:Key="DownStyle" TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Converter={StaticResource LostColumnValueConverter}}" Value="Somenumber">
<Setter Property="Background" Value="{Binding Converter={StaticResource BackgroundConverter}}" />
</DataTrigger>
</Style.Triggers>
</Style>
I think I understand the need to have the Trigger actually bind to something that's going to be changing (in this case I feel I'm forced to also use a ValueConverter to get the column's value, is there a more direct way?), but even if I do this, what do I specify as the DataTrigger's value?
Edit:
So in my case I went ahead and did the following (currently this would only modify the TextBlock's background):
<DataGridTemplateColumn Header="Lost" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Lost, NotifyOnTargetUpdated=True}">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource BackgroundConverter}}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Which to me seems right, but only works once for some reason. I added a seperate TargetUpdated event handler right on the TextBlock definition to see if the event was indeed being called on every change and it is.
Something must be missing on my EventTrigger. Probably something to do with the Storyboard.
Anyways, this all seems incredibly verbose for something so simple, so I went ahead and went with the code-behind route.
I suppose Triggers and DataTriggers can't help in this case because datagrigger's value is unstable. And worse, it's not dependency property and you can't bind to it. It seems to me the better way is to use EventTrigger.
<Window.Resources>
<BeginStoryboard x:Key="bsbPing">
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="FontSize" To="{Binding Path=PingValue}" />
</Storyboard>
</BeginStoryboard>
</Window.Resources>
<Grid>
<StackPanel>
<TextBox Name="txbPingValue" Text="{Binding Path=PingValue}">
<TextBox.Triggers>
<EventTrigger RoutedEvent="TextBox.TextChanged">
<EventTrigger.Actions>
<StaticResource ResourceKey="bsbPing" />
</EventTrigger.Actions>
</EventTrigger>
</TextBox.Triggers>
</TextBox>
<Button Name="btnPing" Click="btnPing_Click">Ping</Button>
</StackPanel>
</Grid>
and code:
public partial class Window7 : Window
{
public Ping MyPing { get; set; }
public Window7()
{
InitializeComponent();
MyPing = new Ping { PingValue = 20.0 };
this.DataContext = MyPing;
}
private void btnPing_Click(object sender, RoutedEventArgs e)
{
MyPing.PingValue += 10;
}
}
public class Ping : INotifyPropertyChanged
{
private double pingValue;
public double PingValue
{
get
{
return pingValue;
}
set
{
pingValue = value;
NotifyPropertyChanged("PingValue");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You are on the right track with the converter. The technique you are using is sometimes called "binning" which maps a large set of integers to a fixed small set of values. It is also used to create histograms.
As far as not updating, your binding expression has to be more specific to retrieve the Lost property, otherwise it won't be re-evaluated when Lost changes. Once you change it you'll also need to update your converter.

Dependency Property Value Precedence and animations

I am animating some property using DoubleAnimation. Before animation is triggered any local or Setter changes are properly reflected in the property. After animation completes nothing seems to be able to change the value of the property. I have even tried ClearValue and InvalidateProperty as well set calling SetValue but the value leftover from animation persists. If animation is repeated, the property continues to be animated as expected so it only appears to be locked for non-animation changes.
Is there a way to rectify this behavior? I want to use the animation to change the property value but still be able to change it manually or via a Setter to anything else. I know a thing or two about Dependency Property Value Precedence but the behavior I am currently experiencing is a bit strange. I'd hate to have to use "manual animations".
EDIT: Added sample XAML + code.
<Window x:Class="WpfApplication7.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
x:Name="_this"
Background="Red">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center" >
<Button Click="ToggleOnClick">Toggle!</Button>
<Button Click="SetHalfOnClick">Set to 0.5!</Button>
</StackPanel>
<TextBox DockPanel.Dock="Bottom" IsReadOnly="True" Text="{Binding ElementName=_viewbox,Path=Opacity}" />
<Viewbox x:Name="_viewbox">
<Viewbox.Style>
<Style TargetType="{x:Type FrameworkElement}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsToggled,ElementName=_this,Mode=OneWay}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="0.2"
Duration="0:0:0.5"
Storyboard.TargetProperty="(UIElement.Opacity)" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="1"
Duration="0:0:0.5"
Storyboard.TargetProperty="(UIElement.Opacity)" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Viewbox.Style>
<TextBlock Text="Sample!" />
</Viewbox>
</DockPanel>
</Window>
Here is the code:
using System.Windows;
namespace WpfApplication7
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1
{
public bool IsToggled
{
get { return (bool)GetValue(IsToggledProperty); }
set { SetValue(IsToggledProperty, value); }
}
public static readonly DependencyProperty IsToggledProperty = DependencyProperty.Register("IsToggled", typeof(bool), typeof(Window1), new UIPropertyMetadata(false));
public Window1()
{
InitializeComponent();
}
private void ToggleOnClick(object sender, RoutedEventArgs e)
{
IsToggled = !IsToggled;
}
private void SetHalfOnClick(object sender, RoutedEventArgs e)
{
_viewbox.Opacity = 0.5;
}
}
}
Edit 2 in response to comments:
In your example you can work around the problem by:
Setting FillBehaviour to Stop on the animation
Adding a handler in code to the Completed event:
<Storyboard Completed="FadeOut_Completed">
Finally, set the desired 'final' value in the Completed handler (either explicitly or by using the current value of the property
private void FadeOut_Completed(object sender, EventArgs e)
{
_viewbox.Opacity = _viewbox.Opacity; //this sets the DP value to the animated value
}
This works in your sample; hopefully it will work in your problem!
Original Answer
If you set the FillBehaviour property of the Storyboard to Stop (instead of the default value of HoldEnd) it will revert to the pre-animation value of the property once the animation completes. HoldEnd causes the animation to maintain its final value on the property
Update in response to comments:
As noted in the comments, the animation value will override the value set against the property when HoldEnd is specified as the FillBehaviour.
This makes it slightly tricky to set the value to something else.
I am not sure if there is a better way to achieve this, but the example below shows one way to work around it. Its hard to judge how applicable this is without a sample usage from the OP, but in this example I am animating the width of a Rectangle on load, and then resetting it to another value when a button is clicked:
<Window x:Class="WpfApplication1.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.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Target" Storyboard.TargetProperty="Width"
From="10" To="100" Duration="0:00:01" FillBehavior="HoldEnd" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Rectangle Height="10" Width="10" Fill="Red" x:Name="Target"/>
<Button Grid.Row="1" Content="Resize">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="Target" Storyboard.TargetProperty="Width">
<DiscreteDoubleKeyFrame Value="50" KeyTime="0:00:00" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</Grid>
</Window>
This works because the new animation overrides the value set in the original.
You can use UIElement.BeginAnimation with the animation parameter set to null. It will clear all animations attached to your property.

WPF - Making an animation's execution conditional on a property of the bound data item

I have a data object -- a custom class called Notification -- that exposes a IsCritical property. The idea being that if a notification will expire, it has a period of validity and the user's attention should be drawn towards it.
Imagine a scenario with this test data:
_source = new[] {
new Notification { Text = "Just thought you should know" },
new Notification { Text = "Quick, run!", IsCritical = true },
};
The second item should appear in the ItemsControl with a pulsing background. Here's a simple data template excerpt that shows the means by which I was thinking of animating the background between grey and yellow.
<DataTemplate DataType="Notification">
<Border CornerRadius="5" Background="#DDD">
<Border.Triggers>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</DataTemplate>
What I'm unsure about is how to make this animation conditional upon the value of IsCritical. If the bound value is false, then the default background colour of #DDD should be maintained.
The final part of this puzzle is... DataTriggers. All you have to do is add one DataTrigger to your DataTemplate, bind it to IsCritical property, and whenever it's true, in it's EnterAction/ExitAction you start and stop highlighting storyboard. Here is completely working solution with some hard-coded shortcuts (you can definitely do better):
Xaml:
<Window x:Class="WpfTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Notification Sample" Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="NotificationTemplate">
<Border Name="brd" Background="Transparent">
<TextBlock Text="{Binding Text}"/>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCritical}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Name="highlight">
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"
Storyboard.TargetName="brd"
From="#DDD" To="#FF0" Duration="0:0:0.5"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="highlight"/>
</DataTrigger.ExitActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding Notifications}"
ItemTemplate="{StaticResource NotificationTemplate}"/>
<Button Grid.Row="1"
Click="ToggleImportance_Click"
Content="Toggle importance"/>
</Grid>
</Window>
Code behind:
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace WpfTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new NotificationViewModel();
}
private void ToggleImportance_Click(object sender, RoutedEventArgs e)
{
((NotificationViewModel)DataContext).ToggleImportance();
}
}
public class NotificationViewModel
{
public IList<Notification> Notifications
{
get;
private set;
}
public NotificationViewModel()
{
Notifications = new List<Notification>
{
new Notification
{
Text = "Just thought you should know"
},
new Notification
{
Text = "Quick, run!",
IsCritical = true
},
};
}
public void ToggleImportance()
{
if (Notifications[0].IsCritical)
{
Notifications[0].IsCritical = false;
Notifications[1].IsCritical = true;
}
else
{
Notifications[0].IsCritical = true;
Notifications[1].IsCritical = false;
}
}
}
public class Notification : INotifyPropertyChanged
{
private bool _isCritical;
public string Text { get; set; }
public bool IsCritical
{
get { return _isCritical; }
set
{
_isCritical = value;
InvokePropertyChanged("IsCritical");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void InvokePropertyChanged(string name)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
Hope this helps :).
What I would do is create two DataTemplates and use a DataTemplateSelector. Your XAML would be something like:
<ItemsControl
ItemsSource="{Binding ElementName=Window, Path=Messages}">
<ItemsControl.Resources>
<DataTemplate
x:Key="CriticalTemplate">
<Border
CornerRadius="5"
Background="#DDD">
<Border.Triggers>
<EventTrigger
RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD"
To="#FF0"
Duration="0:0:0.7"
AutoReverse="True"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock
Text="{Binding Path=Text}" />
</Border>
</DataTemplate>
<DataTemplate
x:Key="NonCriticalTemplate">
<Border
CornerRadius="5"
Background="#DDD">
<TextBlock
Text="{Binding Path=Text}" />
</Border>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplateSelector>
<this:CriticalItemSelector
Critical="{StaticResource CriticalTemplate}"
NonCritical="{StaticResource NonCriticalTemplate}" />
</ItemsControl.ItemTemplateSelector>
And the DataTemplateSelector would be something similar to:
class CriticalItemSelector : DataTemplateSelector
{
public DataTemplate Critical
{
get;
set;
}
public DataTemplate NonCritical
{
get;
set;
}
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
Message message = item as Message;
if(item != null)
{
if(message.IsCritical)
{
return Critical;
}
else
{
return NonCritical;
}
}
else
{
return null;
}
}
}
This way, WPF will automatically set anything that is critical to the template with the animation, and everything else will be the other template. This is also generic in that later on, you could use a different property to switch the templates and/or add more templates (A Low/Normal/High importance scheme).
It seems to be an odity with ColorAnimation, as it works fine with DoubleAnimation. You need to explicity specify the storyboards "TargetName" property to work with ColorAnimation
<Window.Resources>
<DataTemplate x:Key="NotificationTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsCritical}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
Storyboard.TargetName="border"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
<Border x:Name="border" CornerRadius="5" Background="#DDD" >
<TextBlock Text="{Binding Text}" />
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl x:Name="NotificationItems" ItemsSource="{Binding}" ItemTemplate="{StaticResource NotificationTemplate}" />
</Grid>
Here's a solution that only starts the animation when the incoming property update is a certain value. Useful if you want to draw the user's attention to something with the animation, but afterwards the UI should return to it's default state.
Assuming IsCritical is bound to a control (or even an invisible control) you add NotifyOnTargetUpdated to the binding and tie an EventTrigger to the Binding.TargetUpdated event. Then you extend the control to only fire the TargetUpdated event when the incoming value is the one you are interested in. So...
public class CustomTextBlock : TextBlock
{
public CustomTextBlock()
{
base.TargetUpdated += new EventHandler<DataTransferEventArgs>(CustomTextBlock_TargetUpdated);
}
private void CustomTextBlock_TargetUpdated(object sender, DataTransferEventArgs e)
{
// don't fire the TargetUpdated event if the incoming value is false
if (this.Text == "False") e.Handled = true;
}
}
and in the XAML file ..
<DataTemplate>
..
<Controls:CustomTextBlock x:Name="txtCustom" Text="{Binding Path=IsCritical, NotifyOnTargetUpdated=True}"/>
..
<DataTemplate.Triggers>
<EventTrigger SourceName="txtCustom" RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>..</Storyboard>
</BeginStoryboard>
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
You use style triggers in this case. (I'm doing this from memory so there might be some bugs)
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding IsCritical}" Value="true">
<Setter Property="Triggers">
<Setter.Value>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>

Resources