Reputation: 1734
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
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>
Upvotes: 1
Views: 296
Reputation: 1734
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.
Upvotes: 0