mike
mike

Reputation: 1734

Why is this WPF animation only triggered once?

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>

Upvotes: 1

Views: 296

Answers (1)

mike
mike

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

Related Questions