다음을 통해 공유


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