Search code examples
c#wpfdata-bindingxamlcustom-controls

XAML binding doesn't seem to set if the property is initialized in the constructor


I've run into a problem with data-binding inside control template while the property is initialized inside the constructor.

Here is the show-case (you can also download sample solution):

CustomControl1.cs

public class CustomControl1 : ContentControl
{
    static CustomControl1()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(CustomControl1), 
            new FrameworkPropertyMetadata(typeof(CustomControl1)));
    }

    public CustomControl1()
    {
        Content = "Initial"; // comment this line out and everything 
                             // will start working just great
    }
}

CustomControl1 style:

<Style TargetType="{x:Type local:CustomControl1}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomControl1}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ContentPresenter />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

CustomControl2.cs:

public class CustomControl2 : ContentControl
{
    static CustomControl2()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(CustomControl2), 
            new FrameworkPropertyMetadata(typeof(CustomControl2)));
    }
}

CustomControl style:

<Style TargetType="{x:Type local:CustomControl2}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomControl2}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <local:CustomControl1 
                        Content="{Binding Content, 
                            RelativeSource={RelativeSource 
                                    AncestorType=local:CustomControl2}}" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Window1.xaml:

<Window x:Class="WpfApplication5.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300"
        xmlns:local="clr-namespace:WpfApplication5">
    <Grid>
        <local:CustomControl2 Content="Some content" />
    </Grid>
</Window>

So, the problem is: when you launch the app, the content of CustomControl1 appears to be "Initial" which is set by constructor, not the "Some content" string, which is supposed to be set by binding.

When we remove the initialization from the constructor, the binding starts working.

First of all, let me predict the answer: "you should set the initial value of a dependency property inside its metadata: either at the moment of registration or by means of metadata overriding capabilities". Yeap, you right, but the problem with this method of initialization is that the property is of collection type, so if I'll provide new MyCustomCollection() as a default value of the property, then every instance of CustomControl1 will share the same instance of that collection and that's obviously not the idea.

I've done some debugging on the problem, here are the results:

  • Binding instance is created, when we put it in element-like syntax and assign x:Name to it, then it's accessible through Template.FindName("PART_Binding", this) inside OnApplyTemplate.
  • Binding simply isn't set on the property: inside the same OnApplyTemplate the code this.GetBindingExpression(ContentProperty) return null.
  • There is nothing wrong with the binding itself: inside OnApplyTemplate we can look it up and then we can simply set it on the property like this: this.SetBinding(ContentProperty, myBinding) - everything will work fine.

Can anyone explain how and why that happens?

Does anyone have a solution for setting non-shared initial value for a dependency property, so the binding wouldn't break?

Thanks in advance!

UPD: The most weird thing is that debug output with highest trace-level is the same for both cases: either when the binding doesn't occur or if it does.

Here it goes:

System.Windows.Data Warning: 52 : Created BindingExpression (hash=18961937) for Binding (hash=44419000)
System.Windows.Data Warning: 54 :   Path: 'Content'
System.Windows.Data Warning: 56 : BindingExpression (hash=18961937): Default mode resolved to OneWay
System.Windows.Data Warning: 57 : BindingExpression (hash=18961937): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 58 : BindingExpression (hash=18961937): Attach to WpfApplication5.CustomControl1.Content (hash=47980820)
System.Windows.Data Warning: 62 : BindingExpression (hash=18961937): RelativeSource (FindAncestor) requires tree context
System.Windows.Data Warning: 61 : BindingExpression (hash=18961937): Resolve source deferred
System.Windows.Data Warning: 63 : BindingExpression (hash=18961937): Resolving source 
System.Windows.Data Warning: 66 : BindingExpression (hash=18961937): Found data context element: <null> (OK)
System.Windows.Data Warning: 69 :     Lookup ancestor of type CustomControl2:  queried Border (hash=11653293)
System.Windows.Data Warning: 69 :     Lookup ancestor of type CustomControl2:  queried CustomControl2 (hash=54636159)
System.Windows.Data Warning: 68 :   RelativeSource.FindAncestor found CustomControl2 (hash=54636159)
System.Windows.Data Warning: 74 : BindingExpression (hash=18961937): Activate with root item CustomControl2 (hash=54636159)
System.Windows.Data Warning: 104 : BindingExpression (hash=18961937):   At level 0 - for CustomControl2.Content found accessor DependencyProperty(Content)
System.Windows.Data Warning: 100 : BindingExpression (hash=18961937): Replace item at level 0 with CustomControl2 (hash=54636159), using accessor DependencyProperty(Content)
System.Windows.Data Warning: 97 : BindingExpression (hash=18961937): GetValue at level 0 from CustomControl2 (hash=54636159) using DependencyProperty(Content): 'Some content'
System.Windows.Data Warning: 76 : BindingExpression (hash=18961937): TransferValue - got raw value 'Some content'
System.Windows.Data Warning: 85 : BindingExpression (hash=18961937): TransferValue - using final value 'Some content'

UPD2: added a link to the sample solution


Solution

  • I've crossposted the problem at MSDN forums, someone there has suggested to create an issue at Microsft Connect... Long story short: the key mechanism I didn't clearly understand was DP's value precedence. It is perfectly described here (local value has higher priority than templated parent's value).

    Second, not really obvious moment is that the value is considered as templated parent's if it was set by any template (not even element's own template).

    HTH.