[WPF] 解決在 Trigger 中無法使用 TemplateBinding 的方式

  • 1408
  • 0

習慣寫 ControlTemplate 的朋友應該都滿習於使用  TemplateBinding,但是 TemplateBinding 有某些限制導致無法使用在 Trigger 中,本篇用個簡單的範例來說明如何解決類似的問題。

為了解說方便,把情境縮小到一個簡單的需求 -- 我們需要製作一個自訂控制項。需求是當滑鼠移到這個控制項上面的時候,會改變背景顏色。

首先我們建立一個自訂控制項,基本上用的就是 Visual Studio WPF自訂控制項範本產出來的 Template (參考 Sample001):

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

傳統上我們會直接建立一個 Style 來控制不同的 Background,類似這樣:

<Window.Resources >
    <Style TargetType="local:MyControl" x:Key="MyControlBaseStyle">
        <Setter Property="Width" Value="40"/>
        <Setter Property="Height" Value="40"/>
        <Setter Property="Margin" Value="12"/>
        <Setter Property="BorderBrush" Value="DarkGray"/>
        <Setter Property="BorderThickness" Value="2"/>
        <Style.Triggers >
            <Trigger Property="IsMouseOver" Value="False">
                <Setter Property="Background" Value="Red" />
            </Trigger>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="Blue"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<StackPanel >
   <local:MyControl Style="{StaticResource MyControlBaseStyle}"/>
</StackPanel>

看起來挺簡單,如果需求是需要好幾個相同的控制項,但是背景顏色的變換不一樣,也許我們會這麼做 (參考 Sample002):

<Window.Resources >
    <Style TargetType="local:MyControl" x:Key="MyControlBaseStyle">
        <Setter Property="Width" Value="40"/>
        <Setter Property="Height" Value="40"/>
        <Setter Property="Margin" Value="12"/>
        <Setter Property="BorderBrush" Value="DarkGray"/>
        <Setter Property="BorderThickness" Value="2"/>
    </Style>
    <Style TargetType="local:MyControl" x:Key="MyControlRedBlueStyle" BasedOn="{StaticResource MyControlBaseStyle}">
        <Style.Triggers >
            <Trigger Property="IsMouseOver" Value="False">
                <Setter Property="Background" Value="Red" />
            </Trigger>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="Blue"/>
            </Trigger>
        </Style.Triggers>
    </Style>
    <Style TargetType="local:MyControl" x:Key="MyControlWhiteBlackStyle" BasedOn="{StaticResource MyControlBaseStyle}">
        <Style.Triggers >
            <Trigger Property="IsMouseOver" Value="False">
                <Setter Property="Background" Value="White" />
            </Trigger>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="Black"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<StackPanel >
    <local:MyControl Style="{StaticResource MyControlRedBlueStyle}"/>
    <local:MyControl Style="{StaticResource MyControlWhiteBlackStyle}"/>
</StackPanel>

若是只有兩個三個形式也就算了,要是這些形式的變化一多,可真是不得了。所以我們來想另外一個方式,如果我們把這個 Trigger 擺在 ControlTemplate 裡事情應該會比較簡單。

Control 基本上只有一個 Background 屬性,所以先為它增加一個另一個背景筆刷的屬性當成是 IsMouseOver 為 True 時的背景,就稱為 AlternativeBackground 吧 (參考 Sample003 ):

public class MyControl : Control
{
    public static readonly DependencyProperty AlternativeBackgroundProperty =
        DependencyProperty.Register(nameof(AlternativeBackground), typeof(Brush), typeof(MyControl),
            new PropertyMetadata(new SolidColorBrush(Colors.Transparent)));
    static MyControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyControl), new FrameworkPropertyMetadata(typeof(MyControl)));
    }

    public Brush AlternativeBackground
    {
        get { return (Brush) GetValue(AlternativeBackgroundProperty); }
        set { SetValue(AlternativeBackgroundProperty, value); }
    }
}

然後就快樂的在 ControlTemplate 裡面加上 Trigger,直覺地利用 TemplateBinding 來繫結 AlternativeBackground 屬性:

<Style TargetType="{x:Type local:MyControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:MyControl}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        x:Name="border">
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Trigger.Setters>
                            <Setter Property="Background" 
                                    Value="{TemplateBinding AlternativeBackground}" 
                                    TargetName="border"/>
                        </Trigger.Setters>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

在 MainWindow.xaml 測試一下成效:

<Window.Resources >
    <Style TargetType="local:MyControl" x:Key="MyControlBaseStyle">
        <Setter Property="Width" Value="40"/>
        <Setter Property="Height" Value="40"/>
        <Setter Property="Margin" Value="12"/>
        <Setter Property="BorderBrush" Value="DarkGray"/>
        <Setter Property="BorderThickness" Value="2"/>
    </Style>
    
</Window.Resources>
<StackPanel >
    <local:MyControl Style="{StaticResource MyControlBaseStyle}" Background="Red" AlternativeBackground="Blue" />
    <local:MyControl Style="{StaticResource MyControlBaseStyle}" Background="White" AlternativeBackground="Black" />
</StackPanel>

一執行,Ooooops .......

令人絕望的結果,這招行不通。

TemplateBinding 是甚麼?

根據微軟的文件  TemplateBinding Markup Extension 的說明其中有這麼一段:

A TemplateBinding is an optimized form of a Binding for template scenarios, analogous to a Binding constructed with {Binding RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}.

簡單來說, TemplateBinding 是針對 Template 情境下的最佳化形式,差不多就和 {Binding RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay} 的作用相同,但它們有一些小小的差別,小到我們平常察覺不出來。但是現在發生了,在 Trigger 裡是沒法使用 TemplateBinding 的,所以我們就試著改成 {Binding RelativeSource={RelativeSource TemplatedParent}} 看看。

Trigger 的  Setter 改成以下的形式:

<Setter Property="Background" 
        Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=AlternativeBackground}" 
        TargetName="border"/>

好了,這樣就解決了在 Trigger 中繫結到 Template 所屬的控制項的屬性了。

範例請參考:TemplateBindingInTriggers

註:在 WPF 4.5 Unleashed 這本書有提到一件事,節錄如下:
Warning: TemplateBinding works only inside a template’s visual tree and doesn’t work with propertieson Freezables!

TemplateBinding doesn’t work outside a template or outside its VisualTree property, so you can’t even use TemplateBinding inside a template’s trigger. Furthermore, TemplateBinding doesn’t work when applied to a Freezable (for mostly artificial reasons). For example, attempting to bind the Color property of any explicit Brush fails.However, TemplateBinding is just a less-powerful but convenient shortcut for using a regular Binding.You can get the same effect by using a regular Binding with a RelativeSource equal to{RelativeSource TemplatedParent} and a Path equal to the dependency property whose value youwant to retrieve. Such a Binding works in the cases mentioned where TemplateBinding does not.