当在控件模板的数据触发器(DataTrigger)中使用TemplatedParent时,其值为空(null)。

7
考虑以下这个(精简版)为一个内容为字符串的按钮设计的样式:Style
<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>

在这个示例中,意图是如果按钮文本等于单词“Test”,则将其文本变为红色1。但是它不起作用,因为触发器的TemplatedParent绑定解析为null,而不是应用样式的Button。然而,名为“demo”的TextBlock将其Text设置为“System.Windows.Controls.Button:[ButtonText]”,这意味着TemplatedParent在那个级别上正常工作。为什么它在DataTrigger内部不起作用呢?
1我知道还有其他方法可以实现,但我试图理解为什么绑定不按照我的预期工作。

如果将 DataTrigger 更改为 Trigger,将 TemplatedParent 更改为 Self 会有帮助吗? - ASh
@ASh 是的,那确实可以工作,但对于我的实际情况,我需要使用 DataTrigger,因为我真正使用的是 TemplatedParent 的非依赖属性。 - dlf
基本上原因是RelativeSource试图通过沿着可视树向上遍历来解析父级,直到达到条件。但在这种情况下,您将绑定应用于非可视元素。 - Dmitry
3个回答

12

ControlTemplate.Triggers 中的 TemplatedParent 不是您预期的内容。在触发器内部,它实际上引用了 Button.TemplatedParent。因此,只有在您在模板中创建 按钮 时,它才会为非 null 值。在您的情况下,您没有在模板中创建按钮,所以它的值为 null。现在考虑以下 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>

在这里我重新定义了ContentControl并在模板内使用了button with your template. 如果您运行此代码,将会看到红色的“dummy”文本,因为Button.TemplatedParent现在是ContentControl,它的Content等于“Test”,这证实了我上面所说的。

现在回到您的问题:只需将RelativeSource TemplatedParent更改为RelativeSource Self(无需将DataTrigger更改为Trigger)- 这将引用您的按钮。


你显然是正确的。这是否意味着触发器“作用域”仅限于按钮本身,即使它在按钮的模板中定义?如果我知道这一点,是否有某些原则可以帮助我预料到这一点? - dlf
1
你可以这样认为:任何在 ControlTemplate(例如 StackPanel)中的内容都是模板的子元素。每当你使用 ControlTemplate 创建新 Button 时,所有这些子元素都会根据模板每次被创建。触发器不是模板的“子元素”,它们是模板定义的一部分,而不是每次创建新按钮时“从模板创建出来”的。你也可以区分可视化控件(模板中的子元素)和非可视化控件(触发器)。 - Evk

1

我认为这可能是 .NET Core WPF 中的类似问题。

我的 DataTrigger 在使用 {Binding MyProp, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource Convert}} 时没有触发,但是当我将 RelativeSource 更改为 Self 时,绑定开始工作了。我不确定它是一个 hack 还是一个解决方案,但它起作用了。

也许值得一提的是,我的模板基于 MyView(见下文),我正在绑定到 MyView 上的一个 DependencyProperty

因此,我的最终代码如下:

<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>

-1

我不太确定,但我认为触发器是按引用相等的,因为 Content 返回一个对象。所以,您在触发器中定义的字符串将永远不会与其相等。


很确定,由于String实现了IEquatable<String>接口,WPF将使用String.Equals()进行比较。但无论如何,如果我将DataTrigger更改为普通的Trigger,则该方法可以正常工作。此外,通过使用QuickConverter,我可以确认在那种情况下(只有那种情况),TemplatedParent绑定正在评估为空。 - dlf
嗯,那是有道理的。 但是当我使用DataTemplates时,遇到了类似的问题。我无法在模板中使用控件上的DataTriggers。这是因为触发器位于与模板本身不同的命名空间(逻辑树)中。也许这就是这里出现问题的原因。 - Bass Guru

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接