Wednesday, August 16, 2023

What actually happens when you change a Dependency Property?

I found myself trying to explain to a colleague what the sequence of events is when you change a dependency property. Obviously it will be a bit different when you change a Dependency Property by changing the bound property vs changing it internally. I found I wasn't entirely sure what got called when.

So I created a simple WPF app that writes to Debug as control passed from the property to the dependency property and back. It explains the process quite clearly.

Start by creating a new WPF, C#, .Net Core application called BindingEvents in Visual Studio. Add a new class called AlphaTextBox. It subclasses TextBox and adds a DependencyProperty called IsAlphaOnly. We won't be implementing the functionality but we will be adding code to track what gets called and when.

Make the code of AlphaTextBox look like this. You can bind the IsAlphaOnly dependency property and you can also change it internally by double-clicking the text box.

using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
 
namespace BindingEvents
{
    internal class AlphaTextBox : TextBox
    {
        public static readonly DependencyProperty IsAlphaOnlyProperty = DependencyProperty.Register(nameof(IsAlphaOnly), typeof(bool), typeof(AlphaTextBox), new PropertyMetadata(false, IsAlphaOnly_Changed));
 
        private static void IsAlphaOnly_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Debug.WriteLine("IsAlphaOnlyDependencyProperty_Changed");
        }
 
        public bool IsAlphaOnly
        {
            get
            {
                Debug.WriteLine("IsAlphaOnlyDependencyProperty_Get");
                return (bool)GetValue(IsAlphaOnlyProperty);
 
            }
            set
            {
                Debug.WriteLine("IsAlphaOnlyDependencyProperty_Set_In");
                SetValue(IsAlphaOnlyProperty, value);
                Debug.WriteLine("IsAlphaOnlyDependencyProperty_Set_out");
            }
        }
 
        public AlphaTextBox()
        {
            this.MouseDoubleClick += AlphaTextBox_MouseDoubleClick;
        }
 
        private void AlphaTextBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            Debug.WriteLine("AlphaTextBox_MouseDoubleClick_In");
            this.IsAlphaOnly = !this.IsAlphaOnly;
            Debug.WriteLine("AlphaTextBox_MouseDoubleClick_Out");
        }
    }
}
 
Make the XAML and code in MainWindow look like this.

<Window x:Class="BindingEvents.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BindingEvents"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="ToggleAlphaOnlyCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource ToggleAlphaOnlyCommand}" Executed="ToggleAlphaOnly_Executed"/>
    </Window.CommandBindings>
    <StackPanel Orientation="Horizontal" Height="23">
        <local:AlphaTextBox Width="100" Text="{Binding Text}" IsAlphaOnly="{Binding IsAlphaOnly}"/>
        <Button Content="Toggle Alpha Only" Command="{StaticResource ToggleAlphaOnlyCommand}"/>
    </StackPanel>
</Window>


using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows;
 
namespace BindingEvents
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private String _Text;
        public String Text
        {
            get => _Text;
            set => SetProperty(ref _Text, value);
        }
 
        private bool _IsAlphaOnly;
        public bool IsAlphaOnly
        {
            get
            {
                Debug.WriteLine("IsAlphaOnly_Get");
                return _IsAlphaOnly;
            }
            set
            {
                Debug.WriteLine("IsAlphaOnly_Set_In");
                SetProperty(ref _IsAlphaOnly, value);
                Debug.WriteLine("IsAlphaOnly_Set_Out");
            }
        }
 
        public MainWindow()
        {
            Debug.WriteLine("----------------------------------------");
            InitializeComponent();
        }
 
        public event PropertyChangedEventHandler? PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            Debug.WriteLine("SetProperty");
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                {
                    Debug.WriteLine("Raising PropertyChanged");
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
                    Debug.WriteLine("Raised PropertyChanged");
                }
            }
        }
 
        private void ToggleAlphaOnly_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            Debug.WriteLine("ToggleAlphaOnly_Executed_In");
            IsAlphaOnly = !IsAlphaOnly;
            Debug.WriteLine("ToggleAlphaOnly_Executed_Out");
        }
    }
}

If you run this application you see a text box and a button. You can toggle the IsAlphaOnly property by clicking on the button or by double-clicking in the text box.


Let's see what happens when you click the button. This changes the bound property. The output is in the Immediate Window.

ToggleAlphaOnly_Executed_In
IsAlphaOnly_Get
IsAlphaOnly_Set_In
SetProperty
Raising PropertyChanged
IsAlphaOnly_Get
IsAlphaOnlyDependencyProperty_Changed
Raised PropertyChanged
IsAlphaOnly_Set_Out
ToggleAlphaOnly_Executed_Out

As we can see, calling the setter raises the Property_Changed event which calls the Changed event handler for the dependency property. Any code that needs to respond to external changes to the Dependency Property must be executed from the Changed event handler.

Now we can double-click in the TextBox to see what happens when the control itself modifies the DependencyProperty.

AlphaTextBox_MouseDoubleClick_In
IsAlphaOnlyDependencyProperty_Get
IsAlphaOnlyDependencyProperty_Set_In
IsAlphaOnlyDependencyProperty_Changed
IsAlphaOnlyDependencyProperty_Set_out
AlphaTextBox_MouseDoubleClick_Out

Here we see the DependencyProperty setter is called and, when it calls SetValue, the Changed event handler is called.. Any code that needs to be executed whenever the DependencyProperty is changed, regardless of whether the change was internal or external, needs to be in the Changed event handler. If you need to behave differently for internally and externally caused changes, you can use the DependencyProperty setter.

You will notice the SetProperty method does not raise PropertyChanged if the property does not appear to change. In ToggleAlphaOnly_Executed change the code to...

IsAlphaOnly = IsAlphaOnly;

Now run the application and click the button.

ToggleAlphaOnly_Executed_In
IsAlphaOnly_Get
IsAlphaOnly_Set_In
SetProperty
IsAlphaOnly_Set_Out
ToggleAlphaOnly_Executed_Out

You will notice PropertyChanged is not raised. This is a common pattern to use. Now let's modify SetProperty to always raise PropertyChanged.

//if (!Object.Equals(storage, value))

If we run the application again and click the button we see this trace.

ToggleAlphaOnly_Executed_In
IsAlphaOnly_Get
IsAlphaOnly_Set_In
SetProperty
Raising PropertyChanged
IsAlphaOnly_Get
Raised PropertyChanged
IsAlphaOnly_Set_Out
ToggleAlphaOnly_Executed_Out

We can see that PropertyChanged is raised, but the DependencyProperty is smart enough to realize the value did not change and it does not raise the Changed event and the Changed event handler is not executed. That line of code we just commented out in SetProperty is not really necessary.

Hopefully this explains when the DependencyProperty changed event is raised and when its Setter gets called.