How to target a template setter at non-element content
Here’s a technique you can follow to use property triggers in a template on non-element type objects. First, though, some background on what that means …
Take this example of a Button with a custom template which is simply a rectangle:
<Button>
<Button.Template>
<ControlTemplate TargetType="Button">
<Rectangle Width='60' Height='40' Fill='Red' x:Name='MyRectangle'>
</Rectangle>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="MyRectangle" Property="Fill" Value="Green"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
… the rectangle is Red, but changes to Green when the mouse goes over it.
Now fill the Rectangle with a LinearGradientBrush, rather than the SolidColorBrush:
<Button>
<Button.Template>
<ControlTemplate TargetType="Button">
<Rectangle Width='60' Height='40' x:Name='MyRectangle'>
<Rectangle.Fill>
<LinearGradientBrush >
<GradientStop Color='Red' />
<GradientStop Color='Blue' Offset='1' x:Name='SecondStop' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="MyRectangle" Property="Fill" Value="Green"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
… and now the Rectangle is red-to-blue by default, but still the Rectangle changes to green when the mouse is over it.
Now say you want the mouse-over behavior to keep LinearGradientBrush, but to instead just modify the second stop to green. One way you could do that would be to have two copies of the LinearGradientBrush:
<Button>
<Button.Template>
<ControlTemplate TargetType="Button">
<Rectangle Width='60' Height='40' x:Name='MyRectangle'>
<Rectangle.Fill>
<LinearGradientBrush >
<GradientStop Color='Red' />
<GradientStop Color='Blue' Offset='1' x:Name='SecondStop' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName='MyRectangle' Property="Fill" >
<Setter.Value>
<LinearGradientBrush >
<GradientStop Color='Red' />
<GradientStop Color= 'Green' Offset='1' />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
But if you have a more complicated object, such as a Drawing, you might not want to have to duplicate the entire thing. The difficulty is that the Setter of a Trigger can only target a FrameworkElement or FrameworkContentElement, not (in this case) a GradientStop. For example, if you tried make the second stop green with a trigger:
<Button>
<Button.Template>
<ControlTemplate TargetType="Button">
<Rectangle Width='60' Height='40' x:Name='MyRectangle'>
<Rectangle.Fill>
<LinearGradientBrush >
<GradientStop Color='Red' />
<GradientStop Color='Blue' Offset='1' x:Name='SecondStop' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="SecondStop" Property="Color" Value="Green"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
… you’ll get the error “Cannot find the Trigger target 'SecondStop'. (The target must appear before any Setters, Triggers, or Conditions that use it.) ”.
There is a way to solve this, though, by using data binding. With a Binding, you can find your way out of the brush to an element, and then the Setter can target that element. For example, the following markup gives us the desired LinearGradientBrush of red-to-blue when the mouse is away from the button, and red-to-green when the mouse is over it:
<Button>
<Button.Template>
<ControlTemplate TargetType="Button">
<Rectangle Width='60' Height='40' x:Name='MyRectangle' >
<Rectangle.Tag>
<Color>Blue</Color>
</Rectangle.Tag>
<Rectangle.Fill>
<LinearGradientBrush >
<GradientStop Color='Red' />
<GradientStop
Color='{Binding ElementName=MyRectangle, Path=Tag}'
Offset='1' x:Name='SecondStop' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="MyRectangle" Property="Tag" Value="Green"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
Comments
Anonymous
May 22, 2008
I know this is an old post, but hopefully you'll see this anyways.. Your 2nd and 4th XAML samples are identical, which leads me to think that you had a mispaste for your 2nd sample. This post was very helpful in explaining how to get around the restriction of not being able to target non-element type objects. The only thing I am still confused about and am having trouble finding is what does this restriction mean in the first place? The error is "The target must appear before any Setters, Triggers, or Conditions that use it.", but in the 4th XAML sample 'SecondStop' is clearly defined before the Setter. Does this error message mean something else entirely?Anonymous
May 22, 2008
You're right, copy/paste error; I fixed the second Xaml. Thanks for letting me know. The error message is unfortunately confusing, which is actually part of what motivated me to write this post in the first place. The real problem is that it can't find a FrameworkElement with the name 'SecondStop'. The parenthetical in the message is a suggestion of one possible cause.Anonymous
September 27, 2012
Good one mike.