When I used to write a lot of Ajax for ASP.NET (many years ago) I quite like the feature whereby you could disable the page and show a ‘loading spinner’ while waiting for an update in the page. This is a great feature to have in WPF, so I have created a ViewModel with a ‘loading’ switch (bool), and an attached property that creates and displays the animated spinner (for the duration of the switch flag being true) Firstly, we need a control template for a content control that will be our animated spinner. <Style x:Key="LoadingSpinner" TargetType="ContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid>
<Rectangle Width="160" Height="160">
<Rectangle.Fill>
<VisualBrush Stretch="None">
<VisualBrush.Visual>
<Canvas RenderTransformOrigin="0.5,0.5">
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="71.1667"
Canvas.Top="3.00002" Stretch="Fill" Fill="#FF000000"/>
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="71.1667"
Canvas.Top="139.833" Stretch="Fill" Fill="#85000000"/>
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="139.583"
Canvas.Top="71.4167" Stretch="Fill" Fill="#C2000000"/>
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="2.75"
Canvas.Top="71.4167" Stretch="Fill" Fill="#48000000"/>
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="22.7888"
Canvas.Top="23.0388" Stretch="Fill" Fill="#29000000"/>
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="119.545"
Canvas.Top="119.795" Stretch="Fill" Fill="#A4000000"/>
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="119.545"
Canvas.Top="23.0388" Stretch="Fill" Fill="#E1000000"/>
<Ellipse Width="14.8333" Height="14.8333" Canvas.Left="22.7888"
Canvas.Top="119.795" Stretch="Fill" Fill="#67000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="44.9828"
Canvas.Top="8.20598" Stretch="Fill" Fill="#1A000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="97.3466"
Canvas.Top="134.623" Stretch="Fill" Fill="#94000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="134.373"
Canvas.Top="45.2328" Stretch="Fill" Fill="#D2000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="7.95596"
Canvas.Top="97.5967" Stretch="Fill" Fill="#57000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="7.95596"
Canvas.Top="45.2328" Stretch="Fill" Fill="#39000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="134.373"
Canvas.Top="97.5966" Stretch="Fill" Fill="#B3000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="97.3466"
Canvas.Top="8.20599" Stretch="Fill" Fill="#F0000000"/>
<Ellipse Width="14.8372" Height="14.8372" Canvas.Left="44.9828"
Canvas.Top="134.623" Stretch="Fill" Fill="#76000000"/>
<Canvas.RenderTransform>
<RotateTransform Angle="0" />
</Canvas.RenderTransform>
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="0.6" ScaleY="0.6" />
</Canvas.LayoutTransform>
<Canvas.Triggers>
<EventTrigger RoutedEvent="ContentControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty=
"(Canvas.RenderTransform).Angle"
From="0" To="360" Duration="0:0:02"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
</Canvas>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
<ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The Content property of out content presenter will include our text message that will be inside the spinner
Next we need our attached property class
public class AsyncNotifier
{
public static readonly DependencyProperty TriggerProperty =
DependencyProperty.RegisterAttached("Trigger", typeof(bool), typeof(AsyncNotifier),
new PropertyMetadata(false, TriggerCallback));
public static readonly DependencyProperty SpinnerTextProperty =
DependencyProperty.RegisterAttached("SpinnerText", typeof(string), typeof(AsyncNotifier));
private static readonly DependencyProperty SpinnerProperty =
DependencyProperty.RegisterAttached("Spinner", typeof(Grid), typeof(AsyncNotifier));
public static void SetTrigger(DependencyObject d, bool trigger)
{
d.SetValue(TriggerProperty, trigger);
}
public static void SetSpinnerText(DependencyObject d, string text)
{
d.SetValue(SpinnerTextProperty, text);
}
private static void TriggerCallback(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var parentGrid = d as Grid;
if (parentGrid == null)
return;
string spinnerText = (string)parentGrid.GetValue(SpinnerTextProperty);
bool trigger = (bool)parentGrid.GetValue(TriggerProperty);
Grid grid = parentGrid.GetValue(SpinnerProperty) as Grid;
if (grid == null)
{
grid = new Grid();
parentGrid.SetValue(SpinnerProperty, grid);
if (parentGrid.ColumnDefinitions.Count > 0)
Grid.SetColumnSpan(grid, parentGrid.ColumnDefinitions.Count);
if (parentGrid.RowDefinitions.Count > 0)
Grid.SetRowSpan(grid, parentGrid.RowDefinitions.Count);
}
grid.Background = new SolidColorBrush(Colors.White) { Opacity = 0.6 };
grid.Children.Clear();
ContentControl cont = new ContentControl();
cont.Content = new TextBlock() { Text = spinnerText };
cont.Style = (Style)parentGrid.FindResource("LoadingSpinner");
grid.Children.Add(cont);
if (!parentGrid.Children.Contains(grid))
parentGrid.Children.Add(grid);
grid.Visibility = trigger ? Visibility.Visible : Visibility.Hidden;
}
}
Now our ViewModel
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public bool IsDataLoading { get; private set; }
public List<string> TestData { get; private set; }
public ICommand LoadData { get; private set; }
public MainViewModel()
{
IsDataLoading = false;
LoadData = new ActionCommand(LoadTestData);
}
private void LoadTestData()
{
TestData = new List<string>();
ThreadPool.QueueUserWorkItem((o) =>
{
IsDataLoading = true;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsDataLoading"));
for (int i = 0; i < 20; i++)
Dispatcher.CurrentDispatcher.Invoke((Action)(() => TestData.Add("Test Data Item " + i)));
Thread.Sleep(2000);
IsDataLoading = false;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("IsDataLoading"));
PropertyChanged(this, new PropertyChangedEventArgs("TestData"));
}
});
}
}
public class ActionCommand : ICommand
{
private readonly Action action;
public ActionCommand(Action action)
{
this.action = action;
}
public void Execute(object parameter)
{
action();
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
Heres out Window that uses the attached property to include a spinner
<Window.Resources>
<CollectionViewSource x:Key="DataList" Source="{Binding TestData}" />
</Window.Resources>
<Grid Background="AliceBlue" app:AsyncNotifier.Trigger="{Binding IsDataLoading}"
app:AsyncNotifier.SpinnerText="Loading...">
<TabControl Grid.RowSpan="2">
<TabItem Header="TabItem">
<Grid Background="#FFE5E5E5">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding
Source={StaticResource DataList}}" />
<Button Content="Do Update" HorizontalAlignment="Left" Command="{Binding LoadData}"
VerticalAlignment="Top" Width="75" Grid.Row="1" Margin="0,5" />
</Grid>
</TabItem>
<TabItem Header="TabItem">
<Grid Background="#FFE5E5E5"/>
</TabItem>
</TabControl>
</Grid>
And finally our code behind
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
And here are some screen shots
Before:
During Loading:
When Done:
Dean
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)
Dean