WPF – Using Attached Properties For Animated Data Change Notifications

Working in the investment banking industry, we often need to feed live (changing) market data into our WPF controls, and often there is a requirement that certain movements (in value) are highlighted briefly so as to give a strong visual representation of data changes.

The best way to do this (in my opinion) is to have the TextBlock representing the value ‘blink’ a different colour when the value changes. You would normally include rules that for example stipulate that a movement of more than 3% is required before a notification.
You would also like movements ‘up’ to be shown in a different colour to movements ‘down’.

One key characteristic is that these background changes are very short lived – giving a quick ‘blink’ as a notification.

In fact, to add an extra feature, I have effectively removed the acceleration and deceleration phases of the animation to create a ‘toggle’ style blink (rather than pulse), so it suits our scenario better.

There are several ways that you could achieve this, but my favourite is with attached properties and animations.

Here’s the full code

public class ToggleEase : EasingFunctionBase
{
    protected override Freezable CreateInstanceCore()
    {
        return new ToggleEase();
    }

    protected override double EaseInCore(double normalizedTime)
    {
        if (normalizedTime == 1d)
            return 1d;
        return 0d;
    }
}

public class CellHighlight : DependencyObject
{
    public static readonly DependencyProperty ChangeSourceProperty =
        DependencyProperty.RegisterAttached("ChangeSource", typeof(double),
                                            typeof (CellHighlight),
                                            new PropertyMetadata(double.MinValue, DoPropertyChanged));

    public static readonly DependencyProperty MinimumPercentageMovementProperty =
        DependencyProperty.RegisterAttached("MinimumPercentageMovement", typeof(double),
                                            typeof(CellHighlight),
                                            new PropertyMetadata(0d));

    public static readonly DependencyProperty MovementUpColourProperty =
        DependencyProperty.RegisterAttached("MovementUpColour", typeof(Color),
                                            typeof(CellHighlight),
                                            new PropertyMetadata(Colors.LightGreen));

    public static readonly DependencyProperty MovementDownColourProperty =
        DependencyProperty.RegisterAttached("MovementDownColour", typeof(Color),
                                            typeof(CellHighlight),
                                            new PropertyMetadata(Colors.LightSalmon));

    public static void SetChangeSource(DependencyObject d, double useVal)
    {
        d.SetValue(ChangeSourceProperty, useVal);
    }

    public static void SetMinimumPercentageMovement(DependencyObject d, double perc)
    {
        d.SetValue(MinimumPercentageMovementProperty, perc);
    }

    public static void SetMovementUpColour(DependencyObject d, Color color)
    {
        d.SetValue(MovementUpColourProperty, color);
    }

    public static void SetMovementDownColour(DependencyObject d, Color color)
    {
        d.SetValue(MovementDownColourProperty, color);
    }

    private static void DoPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var oldv = (double) e.OldValue;
        var newv = (double) e.NewValue;

        if (oldv == double.MinValue)
            return;

        var percChange = (double) d.GetValue(MinimumPercentageMovementProperty);
        var upColour = (Color) d.GetValue(MovementUpColourProperty);
        var downColour = (Color) d.GetValue(MovementDownColourProperty);

        var diffPerc = Math.Abs((newv - oldv)/oldv*100);

        if (diffPerc <= percChange)
            return;

        var textblock = (TextBlock)d;

        SolidColorBrush brush = new SolidColorBrush();
        textblock.Background = brush;
        ColorAnimation anim = new ColorAnimation
                    {
                        To = oldv > newv ? downColour : upColour,
                        Duration = TimeSpan.FromMilliseconds(125),
                        AutoReverse = true,
                        EasingFunction = new ToggleEase()
                    };
        brush.BeginAnimation(SolidColorBrush.ColorProperty, anim);

    }

}
<Window x:Class="AnimatedCell.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:AnimatedCell="clr-namespace:AnimatedCell" 
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListView ItemsSource="{Binding}">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" />
                        <GridViewColumn Header="Product Name" DisplayMemberBinding="{Binding ProductName}" />
                        <GridViewColumn Header="Price">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding MktPrice}" 
                                        AnimatedCell:CellHighlight.ChangeSource="{Binding MktPrice}" 
                                        AnimatedCell:CellHighlight.MinimumPercentageMovement="3"
                                        AnimatedCell:CellHighlight.MovementDownColour="Red" 
                                        AnimatedCell:CellHighlight.MovementUpColour="Green"   />
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>
internal class Trade : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private double mktPrice;
    public double MktPrice
    {
        get { return mktPrice; }
        set
        {
            if (mktPrice == value)
                return;
            mktPrice = value;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("MktPrice"));
        }
    }

    public string ID { get; set; }
    public string ProductName { get; set; }

    internal static List<Trade> GetTestDataCollection()
    {
        var rand = new Random(Guid.NewGuid().GetHashCode());
        var data = new List<Trade>();
        Enumerable.Range(0, 10).ToList().ForEach((i) =>
            data.Add(new Trade { ID = rand.Next(1000, 9999).ToString(), 
                mktPrice = rand.NextDouble(), ProductName = "Prod" + i.ToString() }));
        return data;
    }

}
public partial class MainWindow : Window
{
    private readonly DispatcherTimer timer = new DispatcherTimer();
    public MainWindow()
    {
        InitializeComponent();

        var rand = new Random(Guid.NewGuid().GetHashCode());
        var data = Trade.GetTestDataCollection();
        timer.Interval = TimeSpan.FromSeconds(1);
        timer.Tick += (o, e) => data.ForEach((t) => t.MktPrice = rand.NextDouble());

        DataContext = data;

        timer.Start();
    }

}

And heres a quick screen show (obviously you cant see the animation as such)

celanim

 

Dean

Pingbacks and trackbacks (2)+

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

About the author

I am a senior .NET contract (freelance) software developer specialising in WPF and WinRT application development with C#, F#, C++. I mainly work in the Investment Bnking industry building performant and robust user interfaces for front office and middle office systems

RecentComments

Comment RSS

Month List