習慣寫 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 Markup Extension 的說明其中有這麼一段:
簡單來說, 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
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.