WPF/MVVM: Handling Changes To Dependency Properties In The View
Introduction
This post provides an example of how you can perform view related actions in response to a change to an existing dependency property, in this case the TextBlock.Text property, and how you can determine which action to take based on the difference between the old value and current value of this dependency property.
If you want to perform some action whenever a value of some built-in dependency property changes, you can use the static DependencyPropertyDescriptor.FromProperty method to get a reference to a System.ComponentModel.DependencyPropertyDescriptor object and then hook up an event handler using its AddValueChanged method:
<TextBlock x:Name="txtBlock" />
public MainWindow()
{
InitializeComponent();
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor
.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
if (dpd != null)
{
dpd.AddValueChanged(txtBlock, OnTextChanged);
}
}
private void OnTextChanged(object sender, EventArgs e)
{
MessageBox.Show("The value of the Text property of the TextBlock was changed!");
}
The above sample code will display a MessageBox whenever the value of the Text property of the TextBlock changes as a result of the property being set directly or through a binding to some source property.
Now, let’s consider a scenario where you are displaying the present price of a stock in the TextBlock and want to perform a different view related action depending on whether the stock price goes up or down. You might for example want to set the Background property of the TextBlock to System.Windows.Media.Colors.Red when the price goes down and to System.Windows.Media.Colors.Green when the price goes up.
Below is a very simple view model with a single LastPrice property that frequently gets set to some new random values in a background task and a view with a TextBlock that binds to this source property.
View Model
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
this.LastPrice = 40.30M;
Task.Run(() =>
{
Random random = new Random();
while (true)
{
System.Threading.Thread.Sleep(1500); //sleep for 1.5 seconds
int newPrice = random.Next(-50, 50);
this.LastPrice += ((decimal)newPrice / 100);
}
});
}
private decimal _lastPrice;
public decimal LastPrice
{
get { return _lastPrice; }
set { _lastPrice = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
View
<Window x:Class="Mm.Dpd.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBlock x:Name="txtBlock" FontSize="20" Text="{Binding LastPrice}" />
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
The view model implements the System.ComponentModel.INotifyPropertyChanged interface and raises its PropertyChanged event in order for the view to be able to automatically reflect the latest value of the LastPrice property.
Since the DependencyPropertyDescriptor’s AddValueChanged method accepts a simple System.EventHandler delegate, there is no way of getting the old value of the Text property from the System.EventArgs parameter. You could of course save the old value somewhere and then compare it to current value in the event handler in order to determine if the Background property should be set to Red or Green. Another approach is to create your own dependency property with a PropertyChangedCallback and bind this property to the source property of the view model and then bind the Text property of the TextBlock to this new dependency property. You can then get the old value of the property directly from the OldValue property of the System.Windows.DependencyPropertyChangedEventArgs parameter that gets passed to the callback:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
//bind the new dependency property to the source property of the view model
this.SetBinding(MainWindow.MyTextProperty, new Binding("LastPrice"));
//bind the Text property of the TextBlock to the new dependency property
txtBlock.SetBinding(TextBlock.TextProperty, new Binding("MyText") { Source = this });
}
//Dependency property
public static readonly DependencyProperty MyTextProperty =
DependencyProperty.Register("MyText", typeof(string),
typeof(MainWindow),
new FrameworkPropertyMetadata("", new PropertyChangedCallback(OnMyTextChanged)));
//.NET property wrapper
public string MyText
{
get { return (string)GetValue(MyTextProperty); }
set { SetValue(MyTextProperty, value); }
}
//PropertyChangedCallback event handler
private static void OnMyTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
decimal oldValue, newValue;
if (Decimal.TryParse(e.OldValue.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out oldValue)
&& Decimal.TryParse(e.NewValue.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out newValue))
{
MainWindow mainWindow = dependencyObject as MainWindow;
if (mainWindow != null)
{
if (newValue > oldValue)
{
mainWindow.txtBlock.Background = Brushes.Green;
}
else if (newValue < oldValue)
{
mainWindow.txtBlock.Background = Brushes.Red;
}
}
}
}
}
In the above sample code, a new dependency property called “MyText” has been added to the window. Note that the TextBlock’s Text property is now bound to this one and not directly to the LastPrice property of the view model. If you set up the binding between the TextBlock and the “MyText” dependency property in the constructor of the window like in the above sample code, remember to remove the binding from the XAML. Alternatively, you could of course bind to the dependency property of the window in XAML instead of doing it programmatically:
<TextBlock FontSize="20" x:Name="txtBlock"
Text="{Binding Path=MyText, RelativeSource={RelativeSource AncestorType=Window}}"/>
To provide a smoother experience for the user, you could animate the transition from one background colour to another using a System.Windows.Media.Animation.ColorAnimation and a System.Windows.Media.Animation.Storyboard. Animations is out of the scope of this post but below is an example of how it can be done. For more information about animations in WPF, refer to this page on MSDN.
private static void OnMyTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
decimal oldValue, newValue;
if (Decimal.TryParse(e.OldValue.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out oldValue)
&& Decimal.TryParse(e.NewValue.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out newValue))
{
MainWindow mainWindow = dependencyObject as MainWindow;
if (mainWindow != null)
{
ColorAnimation animation = new ColorAnimation();
animation.Duration = TimeSpan.FromSeconds(0.5);
Storyboard storyBoard = new Storyboard();
Storyboard.SetTarget(animation, mainWindow.txtBlock);
Storyboard.SetTargetProperty(animation,
new PropertyPath("(TextBlock.Background).(SolidColorBrush.Color)"));
storyBoard.Children.Add(animation);
if (newValue > oldValue)
{
animation.To = Colors.Green;
}
else if (newValue < oldValue)
{
animation.To = Colors.Red;
}
storyBoard.Begin();
}
}
}
For the above animation to work properly, the Background property of the TextBlock should be set to a System.Windows.Media.SolidColorBrush initially:
<TextBlock FontSize="20" x:Name="txtBlock" Background="Transparent" />
http://magnusmontin.files.wordpress.com/2014/03/greenbg2.png?w=374&h=142
http://magnusmontin.files.wordpress.com/2014/03/redbg.png?w=378&h=142
See also
- Dependency Properties Overview: http://msdn.microsoft.com/en-us/library/ms752914(v=vs.110).aspx
- INotifyPropertyChanged Interface: http://msdn.microsoft.com/en-us/library/vstudio/system.componentmodel.inotifypropertychanged
- Animation Overview: http://msdn.microsoft.com/en-us/library/ms752312(v=vs.110).aspx