DataTrigger does not fire when a property value changes several times during a short window

I found an interesting problem in my WPF application related to MultiDataTrigger not running StoryBoard to animate a data grid cell. I have a WPF data network control that is bound to an ObservableCollection containing POCOs that implement INotifyPropertyChanged.

What i want to achieve

A real-time data grid that blinks when values ​​change. When the value increases, I want the cell to blink green; when the value decreases, I want the cell to be red. The animation simply animates the background color of the cell from solid to transparent with an interval of 1 second.

Problem

Handout does not start MultiDataTrigger after the first time. MultiDataTrigger tracks changes to two properties: IsPositive and HasValueChanged. HasValueChanged is initially false, and later changes from false to true as soon as the Value property is set and the animation works for the first time. After that, HasValueChanged pulsates from false to true to trigger a change notification, but the animation does not start.

Here is the XAML style that I apply to each cell of the data grid:

<Style TargetType="{x:Type TextBlock}">
    <Style.Setters>
        <Setter Property="Background"
                Value="Aqua" />
    </Style.Setters>
    <Style.Triggers>
        <MultiDataTrigger>
            <MultiDataTrigger.Conditions>
                <Condition Binding="{Binding Path=HasValueChanged}"
                            Value="True" />
                <Condition Binding="{Binding Path=IsPositive}"
                            Value="True" />
            </MultiDataTrigger.Conditions>
            <MultiDataTrigger.EnterActions>
                <RemoveStoryboard BeginStoryboardName="PositiveValueCellStoryboard" />
                <RemoveStoryboard BeginStoryboardName="NegativeValueCellStoryboard" />
                <BeginStoryboard Name="PositiveValueCellStoryboard"
                                    Storyboard="{StaticResource PositiveValueCellAnimation}"
                                    HandoffBehavior="SnapShotAndReplace" />
            </MultiDataTrigger.EnterActions>
        </MultiDataTrigger>
        <MultiDataTrigger>
            <MultiDataTrigger.Conditions>
                <Condition Binding="{Binding Path=HasValueChanged}"
                            Value="True" />
                <Condition Binding="{Binding Path=IsPositive}"
                            Value="False" />
            </MultiDataTrigger.Conditions>
            <MultiDataTrigger.EnterActions>
                <RemoveStoryboard BeginStoryboardName="PositiveValueCellStoryboard" />
                <RemoveStoryboard BeginStoryboardName="NegativeValueCellStoryboard" />
                <BeginStoryboard Name="NegativeValueCellStoryboard"
                                    Storyboard="{StaticResource NegativeValueCellAnimation}"
                                    HandoffBehavior="SnapShotAndReplace" />
            </MultiDataTrigger.EnterActions>
        </MultiDataTrigger>
    </Style.Triggers>
</Style>

Here's the XAML for animations:

<Storyboard x:Key="NegativeValueCellAnimation">
    <ColorAnimation Storyboard.TargetProperty="Background.(SolidColorBrush.Color)"
                    Timeline.DesiredFrameRate="10"
                    RepeatBehavior="1x"
                    From="Red"
                    To="Transparent"
                    Duration="0:0:1" />
</Storyboard>

<Storyboard x:Key="PositiveValueCellAnimation">
    <ColorAnimation Storyboard.TargetProperty="Background.(SolidColorBrush.Color)"
                    Timeline.DesiredFrameRate="10"
                    RepeatBehavior="1x"
                    From="Green"
                    To="Transparent"
                    Duration="0:0:1" />
</Storyboard>

Here is the code for the POCO object associated with each cell:

using System;
using System.Threading;
using Microsoft.Practices.Prism.ViewModel;

namespace RealTimeDataGrid
{
    public class Cell : NotificationObject
    {
        public Cell(int ordinal, int value)
        {
            Ordinal = ordinal;
            _value = value;
            LastUpdated = DateTime.MaxValue;
        }

        public void SetValue(int value)
        {
            Value = value;

            // Pulse value changed to get WPF to fire DataTriggers
            HasValueChanged = false;
            Thread.Sleep(100);
            HasValueChanged = true;
        }

        private int _value;

        public int Value
        {
            get { return _value; }
            private set
            {
                if (_value == value)
                    return;

                _value = value;

                // Performance optimization, using lambdas here causes performance issues
                RaisePropertyChanged("IsPositive");
                RaisePropertyChanged("Value");
            }
        }

        private bool _hasValueChanged;

        public bool HasValueChanged
        {
            get { return _hasValueChanged; }
            set
            {
                if (_hasValueChanged == value)
                    return;

                _hasValueChanged = value;

                // Performance optimization, using lambdas here causes performance issues
                RaisePropertyChanged("HasValueChanged");
            }
        }

        public int Ordinal { get; set; }

        public DateTime LastUpdated { get; set; }

        public bool IsPositive
        {
            get { return Value >= 0; }
        }

        public TimeSpan TimeSinceLastUpdate
        {
            get { return DateTime.Now.Subtract(LastUpdated); }
        }
    }
}

Explicit fix

Adding Thread.Sleep (100) in the interval between setting HasValueChanged twice in the SetValue method appears to fix the MultiDataTrigger problem, which does not work, but has unwanted side effects.

Problem video

, .

, .

"" , Thread.Sleep , -, , , . , Thread.Sleep :)

; ? / , ? , , Thread.Sleep( !)?

WPF DataTrigger ? -, WPF "" ; WPF , , ?

!

+5
1

, Thread.Sleep , RoutedEvents ( Changed+Positive Changed+Negative) EventTriggers.


Thread.Sleep, , , :

// IDE-free code, may be broken
HasValueChanged = false;
new Thread((ThreadStart)(() =>
{
    Thread.Sleep(100);
    HasValueChanged = true;
})).Start();
0

All Articles