Tuesday, April 4, 2023

ValueSource - how did that dependency property get its value?

I have a custom control that binds to a complex object and binds itself to various properties in the object. One of my developers needed to be able to override one of these bindings. I wanted my custom control to be able to detect if the developer had explicitly set a Text attribute and not overwrite it with its own binding.

I found the ValueSource structure which is the subject of this interesting page.

You can use GetValueSource to get the ValueSource for a dependency property. If you also combine this with GetBindingExpression which gets the actual Binding Expression, you can figure out a lot about a DependencyProperty. In particular, you can tell if the developer explicitly set a value for a DependencyProperty.

Here's a quick demonstration. Start a new WPF C# project called ValueSource. Set MainWindow xaml and vb to this...

<Window x:Class="ValueSource.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:ValueSource"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <Style TargetType="TextBlock" x:Key="TestBlockStyle">
            <Setter Property="Text" Value="NamedStyle"/>
        </Style>
        <Style TargetType="TextBlock" x:Key="StyleWithTrigger">
            <Style.Triggers>
                <Trigger Property="Visibility" Value="Visible">
                    <Setter Property="Text" Value="StyleWithTrigger"/>
                </Trigger>
            </Style.Triggers>
        </Style>
       
    </Window.Resources>
    <StackPanel Orientation="Vertical">
        <TextBlock Loaded="TextBlock_Loaded"/>
        <TextBlock Text="Literal" Loaded="TextBlock_Loaded"/>
        <TextBlock Text="{Binding TextBlock3_Text}" Loaded="TextBlock_Loaded"/>
        <TextBlock Style="{StaticResource TestBlockStyle}" Loaded="TextBlock_Loaded"/>
        <TextBlock Style="{StaticResource StyleWithTrigger}" Loaded="TextBlock_Loaded"/>
    </StackPanel>
</Window>

-------------------------------------

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
 
namespace ValueSource
{
    public partial class MainWindow : Window
    {
        public String TextBlock3_Text { get { return "BoundText"; } }
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        private void TextBlock_Loaded(object sender, RoutedEventArgs e)
        {
            TextBlock tb = (TextBlock)sender;
            System.Windows.ValueSource vs = DependencyPropertyHelper.GetValueSource(tb, TextBlock.TextProperty);
            BindingExpression be = tb.GetBindingExpression(TextBlock.TextProperty);
            Console.WriteLine($"TextBlock '{tb.Text}' ValueSource is {vs.BaseValueSource.ToString()} and is {(be == null?"not ":"")}bound");
        }
    }
}

We are defining several TextBlock controls and initializing their Text property in different ways. Then we're using GetValueSource and GetBindingExpression to tell us how the Text property was initialized. The output in the Immediate window looks like this...

TextBlock '' ValueSource is Default and is not bound
TextBlock 'Literal' ValueSource is Local and is not bound
TextBlock 'BoundText' ValueSource is Local and is bound
TextBlock 'NamedStyle' ValueSource is Style and is not bound
TextBlock 'StyleWithTrigger' ValueSource is StyleTrigger and is not bound

In my custom control I simply look for a BaseValueSource of Default and I can add my binding. Any other value means the developer has explicitly set a value for Text so I should not override it with my binding.

Here's a full list of possible values for BaseValueSource.


Another way to do this is to use ReadLocalValue. This returns UnsetValue, a Binding Expression, or a value. This technique was suggested by ChatGPT. If you change TextBlock_Loaded to the following...

private void TextBlock_Loaded(object sender, RoutedEventArgs e)
{
    TextBlock tb = (TextBlock)sender;
    object localValue = tb.ReadLocalValue(TextBlock.TextProperty);
    Console.WriteLine($"TextBlock '{tb.Text}' LocalValue is {localValue.ToString()}");
}

The output in the Immediate window now looks like this...

TextBlock '' LocalValue is {DependencyProperty.UnsetValue}
TextBlock 'Literal' LocalValue is Literal
TextBlock 'BoundText' LocalValue is System.Windows.Data.BindingExpression
TextBlock 'NamedStyle' LocalValue is {DependencyProperty.UnsetValue}
TextBlock 'StyleWithTrigger' LocalValue is {DependencyProperty.UnsetValue}

My custom control can apply its own binding if LocalValue is UnsetValue.

No comments:

Post a Comment