Search code examples
c#wpfxamldependency-propertiesrelativesource

GetTemplateChild returns null when dependency property has RelativeSource binding


I have created a custom control, MyTextBox, that inherits from TextBox. It has a style associated with it that contains a namned control:

<Style x:Key="{x:Type MyTextBox}" TargetType="{x:Type MyTextBox}">
    <!-- ... -->
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type MyTextBox}">
                <!-- ... -->
                    <SomeControl x:Name="PART_SomeControl" />
                <!-- ... -->
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

MyTextBox has a dependency property that, when set, propagates its value to SomeControl:

public class MyTextBox : TextBox
{
    // ...

    public static new readonly DependencyProperty MyParameterProperty =
        DependencyProperty.Register(
            "MyParameter",
            typeof(object),
            typeof(MyTextBox),
            new PropertyMetadata(default(object), MyParameterChanged));

    private static void MyParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var me = (MyTextBox)d;
        var someControl = (SomeControl)me.GetTemplateChild("PART_SomeControl");
        someControl.SetValue(SomeControl.MyParameterProperty, e.NewValue);
    }
}

This works fine when doing a simple binding, like this:

<MyTextBox MyParameter="{Binding}" />

But when I use a more fancy binding using RelativeSource, like this:

<MyTextBox MyParameter="{Binding DataContext, RelativeSource={RelativeSource
    FindAncestor, AncestorType=ParentView}}"

the method me.GetTemplateChild() returns null. That is, SomeControl cannot be found.

Why?

One observation I have made is that, when it has a RelativeSource, the MyParameter dependency property is set first of all dependency properties. That is, if I do something like this:

<MyTextBox
    OtherParameter="{Binding}"
    MyParameter="{Binding DataContext, RelativeSource={RelativeSource
        FindAncestor, AncestorType=ParentView}}"

the MyParameter property is (strangely) set before OtherParameter. Using the simple binding they are set in the same order as declared, just as expected.

(As you can see, my code has been stripped from unrelevant stuff. Hopefully, I have included all that is important.)


Solution

  • Most likely it is being set before the template is applied. There are a few ways you could work around that:

    • Call ApplyTemplate before GetTemplateChild to force the template to load.

    • Use BeginInvoke with DispatcherPriority.Loaded to delay the operation until later.

    • Allow MyParameterChanged to fail if there is no template, and repeat the logic in OnApplyTemplate (you should be doing this anyway, in case the template is replaced after load (as in a Windows theme change).

    It looks like you're just passing on the value to a child element. Have you considered using an attached property with value inheritance?

    As for why it fails for your RelativeSource FindAncestor binding, and not the raw DataContext binding, I think this comes down to the fact that DataContext itself is an inherited property. Hypothetically, assume the order of operations is this:

    1. Parent receives DataContext property
    2. Parent adds child
    3. Child evaluates MyParameter
    4. Child applies template
    5. Child inherits DataContext from parent

    In the first case, (MyParameter="{Binding}"), step 3 fails to update MyParameter because it doesn't yet have a DataContext to bind to, so MyParameterChanged is not called and there is no exception. After step 5, when the child's DataContext is updated, it re-evaluates MyParameter, and by that point the template exists so the property change handler works.

    In the second case, you are specifically looking up the DataContext property of the parent, which does exist, so MyParameterChanged is called at step 3 and fails because the template isn't applied yet.