Wednesday, May 27, 2009

Why doesn’t my WPF Trigger work? (... a peek into the Dependency Property evaluation process)

Imagine a simple situation where I have a blue rectangle:

<Grid>
<Rectangle Height="200" Width="300" Fill="Blue"/>
</Grid>



Now when I do a mouse over the rectangle, I want the color to turn to red - simple! The obvious choice in WPF is to add a property trigger. So that’s what I do.


<Grid>
<Grid.Resources>
<Style TargetType="Rectangle" x:Key="rectStyle">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<Rectangle Height="200" Width="300" Fill="Blue"
Style="{StaticResource rectStyle}"/>
</Grid>



But it doesn’t work! What could be wrong? Perhaps the mouse over event isn’t firing on the rectangle for some reason? I debug the trigger using this cool technique and find that the trigger is firing alright. Then what on earth is the problem?

A closer look reveals the real issue. The culprit was the Fill="Blue" assignment! When evaluating the value of a dependency property, the dependency property system takes into consideration a few factors. The whole dependency property value evaluation process is shown below. Certain factors have priority over others as shown in Step 1 below.

Step 1: Determine Base Value
In the order of diminishing priority:
(a) Local Value
(b) Style triggers
(c) Template Triggers
(d) Style Setters
(e) Theme Style Triggers
(f) Theme Style Setters
(g) Inheritance
(h) Default Value

Step 2: Evaluate
– If Step 1 returns expression (DataBinding or DynamicResource), evaluate

Step 3: Apply Animation

Step 4: Coerce
- If CoerceValueCallback defined

Step 5: Validate
- If ValidateValueCallback defined

In the above example of the rectangle, the trigger (see Step 1b) that I set in the style, had lower priority when compared to the local value (see Step 1a), that is the Fill="Blue" assignment. That was the reason why the trigger value was ignored. So the result/output of Step 1 as far as the Fill property was concerned was the local value “Blue”. In this example, Steps 2-5 do not apply.

So how do I get my trigger to work? If I remove the local value assignment for Fill, and instead set that value within the style, then everything will work as expected because triggers (see Step 1b) have higher priority over style setters (see Step 1d).


<Grid>
<Grid.Resources>
<Style TargetType="Rectangle" x:Key="rectStyle">
<Setter Property="Fill" Value="Blue" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<Rectangle Height="200" Width="300"
Style="{StaticResource rectStyle}"/>
</Grid>



Now the trigger works as expected!