TemplatedParent is null when used inside a ControlTemplate's DataTrigger
Asked Answered
P

3

7

Consider this (edited-down) Style, designed for a Button whose Content is a String:

<Style x:Key="Test" TargetType="Button">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
               <StackPanel>
                   <TextBlock x:Name="text" Text="{TemplateBinding Content}" />
                   <TextBlock x:Name="demo" Text="{Binding RelativeSource={RelativeSource TemplatedParent}}" />
                </StackPanel>
                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}">
                        <DataTrigger.Value>
                            <system:String>Test</system:String>
                        </DataTrigger.Value>
                        <Setter TargetName="test" Property="Foreground" Value="Red" />
                    </DataTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The intention in this example is to turn the button text red if it equals the word "Test"1. But it doesn't work, because the trigger's TemplatedParent binding resolves to null instead of to the Button the Style is applied to. However, the TextBlock named "demo" will have its Text set to "System.Windows.Controls.Button: [ButtonText]" as expected, which means TemplatedParent works correctly at that level. Why doesn't it work inside the DataTrigger?


1 I know there are other ways to achieve that, but I'm trying to understand why the binding doesn't work the way I expect it to.
Panettone answered 27/4, 2016 at 17:33 Comment(3)
does it help, if you change 1. DataTrigger -> Trigger and 2. TemplatedParent -> Self in trigger?Atilt
@Atilt Yes that does work, but for my actual case I need to use a DataTrigger because I am really working with a non-DependencyProperty of the TemplatedParent.Panettone
The reason is basically that RelativeSource is trying to resolve a parent by walking up the visual tree till it reach the condition. But in this case you upply the binding on a non visual element.Drachm
S
13

TemplatedParent in your ControlTemplate.Triggers is not what you expect. Inside trigger it actually references Button.TemplatedParent. As such, it will only be non-null if your create that button inside template. You don't create button inside template, so it is null in your case. Now consider this xaml:

<Window.Resources>
    <Style x:Key="Test"
           TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <StackPanel>
                        <TextBlock x:Name="text"
                                   Text="dummy" />
                        <TextBlock x:Name="demo"
                                   Text="{Binding RelativeSource={RelativeSource TemplatedParent}}" />
                    </StackPanel>
                    <ControlTemplate.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}">
                            <DataTrigger.Value>
                                <system:String>Test</system:String>
                            </DataTrigger.Value>
                            <Setter TargetName="text"
                                    Property="Foreground"
                                    Value="Red" />
                        </DataTrigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style x:Key="Test2" TargetType="ContentControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ContentControl">
                    <Button Style="{StaticResource Test}"></Button>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>
<Grid>
    <!--<Button Content="Test" Style="{StaticResource Test}"/>-->
    <ContentControl Style="{StaticResource Test2}" Content="Test" />
</Grid>

Here I retemplate ContentControl and inside template I use button with your template. If you run this code, you will see "dummy" text in red, because Button.TemplatedParent is now ContentControl, and it has it's Content equals "Test", which confirms what I said above.

Now back to your problem: just change RelativeSource TemplatedParent to RelativeSource Self (no need to change DataTrigger to Trigger) - this one would reference your Button.

Sackcloth answered 27/4, 2016 at 19:50 Comment(2)
You are clearly right. Does this imply that the trigger is "scoped" to the Button itself even though it is defined within the Button's Template? Is there some principle in play here that would have lead me to expect that if I'd known it?Panettone
You can think of it like that: whatever is inside ControlTemplate (StackPanel etc) are children of that template. Whenever you create new Button with your ControlTemplate, all those children will be created every time, based on template. Triggers are not "children" of template, they are kind of part of template definition, and not "created out of template" every time you create new Button. You can also make difference between visual controls (those children in template) and non-visual (trigger).Sackcloth
T
1

I think it might be a similar issue in .NET Core WPF. My DataTrigger was not firing with {Binding MyProp, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource Convert}}, but instead when I changed the RelativeSource to Self the binding started to work. I'm not sure whether it's a hack or a solution, but it worked.

Maybe it's worth mentioning that my template was based on MyView (see below) and I was binding to a DependencyProperty on MyView.

So my final code looked like this:

<ControlTemplate x:Key="Template" TargetType="{x:Type ns:MyView}">
    <!-- Some template -->

    <ControlTemplate.Triggers>
        <DataTrigger Binding="{Binding MyProp, RelativeSource={RelativeSource Self}, Converter={StaticResource Convert}}" Value="True">
            <Setter Property="Foreground" Value="Red"/>
        </DataTrigger>
    </ControlTemplate.Triggers>
</ControlTemplate>
Ticknor answered 24/4, 2020 at 12:46 Comment(0)
C
-1

I'm not quite sure, but I think the trigger equals by referenc, because Content returns an Object. So it will never be true with your string defined within the trigger.

Carbamate answered 27/4, 2016 at 17:44 Comment(2)
Pretty sure since String implements IEquatable<String>, WPF will use String.Equals() for the comparison. But in any case, the approach works fine if I change the DataTrigger to an ordinary Trigger. Further, by using QuickConverter, I can confirm that the TemplatedParent binding is evaluating to null in that (but only that) case.Panettone
Well, that makes sense. But I got an similiar problem when i used DataTemplates. I coudn't use DataTriggers on Controls within the Temlpate. That happened because the triggers are in seperate Namespace (Logical Tree) than the Template itself. Maybe that's the same reason here.Carbamate

© 2022 - 2024 — McMap. All rights reserved.