views:

440

answers:

2

Hello, everybody!

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

+1  A: 

Maybe you should use TwoWay binding mode? What should you control do with "Some content"? It can not store it in your control's model since binding is OneWay. in your case binding sees that there is a value in your model's property and takes it overwriting "Some content". If don't initialize property, binding does nothing, because it ignores null values and you see "Some content". I hope my explanation is clear.

EDIT

Sorry for little misunderstanding of your problem. I've downloaded your demo app and reproduced the issue. Reading this and this MSDN articles shows that your intentions were right. However you can find there this words:

The following virtual methods or callbacks are potentially called during the computations of the SetValue call that sets a dependency property value: ValidateValueCallback, PropertyChangedCallback, CoerceValueCallback, OnPropertyChanged.

So, setting value of DependencyProperty in constructor potentially is as dangerous as calling a virtual method of object that is not constructed.

Ok, setting a DependencyProperty in constructor is bad. My next idea was to set value in some callback (I've used OnInitialized since it should be called right after Control's constructor). And I found another really strange behavior. If I don't set any value in constructor (this way)

    public CustomControl1()
    {
        //Content = "Initial1";
    }
    protected override void OnInitialized(EventArgs e)
    {
        Content = "Initial2";
        var check = Content; // after this  check == "Initial_2"
    }

I don't see "Initial2" in the window even if I don't specify any value for Content in Window1.xaml. Notice that value is set correctly (as you I see check it). But if I uncomment Content = "Initial1"; string, I see "Initial2". Also if I initialize Content in OnInitialized binding works fine, but it doesn't resolve that actual value of Content is "Initial2". Looks like its source is not that Content property.

I'll continue working around this issue later. I hope this information can be helpful.

levanovd
Actually, when I bumped into the problem it was in the context of a TwoWay binding. In this sample I've removed all stuff which didn't matter. So, no TwoWay doesn't make it work. Anyway, even if it would have: one-way binding should work in a such way that whenever I change the source, the target should be updated, no matter were there any overridden value or not. But the problem is not about one-time binding failure, it's about binding not setting itself on the property at all. As if there were no binding at all.
archimed7592
A little of off-topic on the sample's ugly design. Please, don't look at non-applicability of the code provided, it's for the demonstration purposes only. The problem emerged in a huge project with perfect meaning for either binding modes or what controls do with their content. I hoped it was obvious, that the sample was intended to help to reproduce the problem.
archimed7592
+1  A: 

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.

archimed7592