Content
Introduction
This series of articles is a deep dive into the possibilities for customization of controls in WPF. Allthough there are allready various articles explaining the possibilities of customizing WPF controls, I wanted this one to provide an extensive explanation on the subject and also to provide examples on what works but also on what doesn't.
Of course, it is up to you to decide if I succeeded.
The article only handles using XAML as a way for customization (with some exceptions if certain things require coding)
Ways of Customization
WPF provides 2 main ways of customizing controls:
- Styling: with Styling you can set various properties on the controls.
- Templating: with Templating you can completely replace the standard look of your control with something else.
For this, I split the series in some parts, for which I currently have the following planned:
- WPF Custom Visualization Part 1 of some: Styling
- WPF Custom Visualization Part 2 of some: Styling with Triggers (this article)
- WPF Custom Visualization Intermezzo: Resources
- WPF Custom Visualization Intermezzo 2: Binding
- WPF Custom Visualization Part 3 of N: Templating
- WPF Custom Visualization Part 4 of N: Addorners
It's all about dynamics now
Ok, so let's go one step further. Userinterfaces for todays applications are all about animations, changing colors depending on state, or short: they change the visuals depending on the state of the application. Enter triggers. They allow you to do something when another thing happens: they are the mediator between what happens and what to do.
That "thing which happens" can be the changing of a property of the control, but also of the datacontext, or even an event which happened
Therefore WPF defines three types of triggers:
Trigger
: allows to change values of properties or execute actions depending on the value of a property of the control on which they are applied. DataTrigger
: allows to change values of properties or execute actions depending on the value of a property of the datacontext of a control on which they are applied. EventTrigger
: allows to execute actions when an event happens (You cannot set properties with this type of trigger)
Property Triggers
Concepts
What Triggers
allow you to do is:
- change certain properties / execute certain actions
- depending on the value of other properties
We thus need following pieces of information
- The property (or multiple properties) to watch
- The value on which to trigger
-
- What property (or multiple properties) to set and their target value.
- Or what action to take when the proeperty gets its value or "loses" its value.
The simplest case is of course when source and target property are on the same object. But there are of course several permutations possible in defining the property to watch and the property to set. Some which immediately come to mind:
- What if the source property and the targetproperty are not on the same object?
- What is the scope of the reference? Can we reference properties of objects in different child trees of a scope.
- Can we reference properties of objects in different windows?
- How to reference properties of objects across exe and dll boundaries
How to do it?
The simplest implementation is the following
<Style x:Key="styleWithTriggerUsingSetter" TargetType="Control">
<Setter Property="Control.Background" Value="Blue" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Foreground" Value="Red" />
<Setter Property="BorderThickness" Value="5" />
</Trigger>
</Style.Triggers>
</Style>
<TextBox Text="Textbox with simple trigger using setter" Style="{StaticResource ResourceKey=styleWithTriggerUsingSetter}"/>
The above definitions will result in following visuals:
As metioned above, it is also possible to specify actions to perform. Those actions are anything derived from TriggerAction
. A basic example is following:
<Style x:Key="styleWithTriggerUsingAction" TargetType="TextBox">
<Setter Property="TextBox.Background" Value="Blue" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)" To="Green" Duration="0:0:0.25" >
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)" To="Red" Duration="0:0:0.25" >
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
<TextBox Text="Textbox with simple trigger using actions" Style="{StaticResource ResourceKey=styleWithTriggerUsingAction}"/>
The above definitions will result in following visuals:
Notice how in the case of actions we have to specify EnterActions
and ExitActions
, where for simple Setters
a single value will suffice.
If you think about this, it is logical: the first are simple property setters, so a simple assignment wil suffice. WPF under the hood remembers the original value and when the trigger condition is no longer valid, just applies the old value again. For actions however, some code is executed and it may not be immediately clear how to undo the action. That is why we have enter actions and leave actions: you can think of them as do-actions and undo-actions.
In the above case, if we would have supplied no ExitActions
, this would have been the result
<Style x:Key="styleWithTriggerUsingActionNoExit" TargetType="TextBox">
<Setter Property="TextBox.Background" Value="Blue" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)" To="Green" Duration="0:0:0.25" >
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
<TextBox Text="Textbox with simple trigger using actions (no exit)" Style="{StaticResource ResourceKey=styleWithTriggerUsingActionNoExit}"/>
The above definitions will result in following visuals:
Of course, there are times when you will want to specify multiple conditions which must be met. For this, there is de MultiTrigger
. You can specify multiple conditions which must all be met: the MultiTrigger
thus effectively performs an AND operation on the conditions:
<Style x:Key="checkBoxStyleWithMultiTrigger">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="CheckBox.IsMouseOver" Value="True"></Condition>
<Condition Property="CheckBox.IsChecked" Value="True"></Condition>
</MultiTrigger.Conditions>
<Setter Property="Control.Foreground" Value="Red" />
</MultiTrigger>
</Style.Triggers>
</Style>
<CheckBox Content="Checkbox with multitrigger" Style="{StaticResource ResourceKey=checkBoxStyleWithMultiTrigger}"/>
The above definitions will result in following visuals:
Specifying an OR condition is simply done by having multiple triggers in the Triggers
collection of the Style
:
<Style x:Key="expliciteMergeStyleWithSomeTriggers">
<Style.Triggers>
<Trigger Property="CheckBox.IsMouseOver" Value="True">
<Setter Property="Control.Foreground" Value="Green" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="CheckBox.IsMouseOver" Value="True"></Condition>
<Condition Property="CheckBox.IsChecked" Value="True"></Condition>
</MultiTrigger.Conditions>
<Setter Property="Control.Foreground" Value="Red" />
</MultiTrigger>
</Style.Triggers>
</Style>
<CheckBox Content="Checkbox with explicite merged multitrigger" Style="{StaticResource ResourceKey=expliciteMergeStyleWithSomeTriggers}"/>
The above definitions will result in following visuals:
There is a caveat here however: because we can have overlapping conditions you must be very carefull with the ordering of your Trigger
s. If multiple Trigger
s fire, then the last one in the list will be applied.
Take for example the above combination: it will be clear that the single condition Property="CheckBox.IsMouseOver" Value="True"
is True
independent of the outcome of the condition <condition property="CheckBox.IsChecked" value="True"></condition>
. As a result, the first Trigger
will fire from the moment the mousepointer is over the control to which the Style
is applied. However, the second MultiTrigger
will fire when the mousepointer is over the control AND the checkbox is checked.
The net result will be that:
- In case the mouse is NOT over the control, the foreground will be the standard color for that control.
- In case the CheckBox IS NOT checked and the mouse is over the control, the foreground will be green
- In case the CheckBox IS checked and the mouse is over the control, the foreground will be red
However, let's switch the ordering of the Triggers:
<Style x:Key="expliciteMergeStyleWithSomeTriggersWrong">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="CheckBox.IsMouseOver" Value="True"></Condition>
<Condition Property="CheckBox.IsChecked" Value="True"></Condition>
</MultiTrigger.Conditions>
<Setter Property="Control.Foreground" Value="Red" />
</MultiTrigger>
<Trigger Property="CheckBox.IsMouseOver" Value="True">
<Setter Property="Control.Foreground" Value="Green" />
</Trigger>
</Style.Triggers>
</Style>
<CheckBox Content="Checkbox with explicite merged multitrigger (wrong ordering)" Style="{StaticResource ResourceKey=expliciteMergeStyleWithSomeTriggersWrong}"/>
Now, if the mouse is over the control, the foreground will ALWAYS be green, irrespective of the state of the CheckBox. In this case the last evaluated condition is the Trigger
which is True
from the moment the mouse is over he control, and thus hides the MultiTrigger
which can be True
at the same time.
This will result in following visuals:
This can lead to some unexpected results, like as if Trigger
is never being fired:
<Style x:Key="styleWithSomeTriggersOrdering1">
<Style.Triggers>
<Trigger Property="CheckBox.IsMouseOver" Value="True">
<Setter Property="Control.Foreground" Value="Green" />
</Trigger>
<Trigger Property="CheckBox.IsChecked" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="styleWithSomeTriggersOrdering2">
<Style.Triggers>
<Trigger Property="CheckBox.IsChecked" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</Trigger>
<Trigger Property="CheckBox.IsMouseOver" Value="True">
<Setter Property="Control.Foreground" Value="Green" />
</Trigger>
</Style.Triggers>
</Style>
<CheckBox Content="Checkbox with trigger and MouseOver/Checked ordering: Checked with green foreground is impossible" Style="{StaticResource ResourceKey=styleWithSomeTriggersOrdering1}"/>
<CheckBox Content="Checkbox with trigger and Checked/MouseOver ordering: Checked with green foreground is possible" Style="{StaticResource ResourceKey=styleWithSomeTriggersOrdering2}"/>
The above definitions will result in following visuals:
It is of course also possible to use the BasedOn
attribute. The final result of this is the merging of the Triggers of the two Styles, with the Triggers of the base Style comming first and those of the other comming last:
<Style x:Key="styleWithTriggerUsingSetter" TargetType="Control">
<Setter Property="Control.Background" Value="Blue" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Foreground" Value="Red" />
<Setter Property="BorderThickness" Value="5" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="basedOnStyleWithTrigger" BasedOn="{StaticResource styleWithTriggerUsingSetter}" TargetType="Control">
<Style.Triggers>
<Trigger Property="Control.IsFocused" Value="True">
<Setter Property="Control.BorderBrush" Value="Green" />
<Setter Property="Control.Foreground" Value="Turquoise" />
</Trigger>
</Style.Triggers>
</Style>
<TextBox Text="Textbox with based on trigger" Style="{StaticResource ResourceKey=basedOnStyleWithTrigger}"/>
The above definitions will result in following visuals:
Notice how the BorderThickness
also changes! The Trigger
from the BasedOn
style is thus not just simply replaced but is effectively merged resulting in 3 Setter
s being executed when it fires. The above trigger is logically identical to following:
<Style x:Key="basedOnStyleWithTriggerExplicite" TargetType="Control">
<Style.Triggers>
<Trigger Property="Control.IsFocused" Value="True">
<Setter Property="BorderThickness" Value="5" />
<Setter Property="Control.BorderBrush" Value="Green" />
<Setter Property="Control.Foreground" Value="Yellow" />
</Trigger>
</Style.Triggers>
</Style>
You can of course also use MultiTrigger
s in the Style
s:
<Style x:Key="checkBoxStyleWithMultiTrigger">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="CheckBox.IsMouseOver" Value="True"></Condition>
<Condition Property="CheckBox.IsChecked" Value="True"></Condition>
</MultiTrigger.Conditions>
<Setter Property="Control.Foreground" Value="Red" />
</MultiTrigger>
</Style.Triggers>
</Style>
<Style x:Key="basedOnMultiStyleWithTrigger" BasedOn="{StaticResource checkBoxStyleWithMultiTrigger}">
<Style.Triggers>
<Trigger Property="CheckBox.IsMouseOver" Value="True">
<Setter Property="Control.Foreground" Value="Green" />
</Trigger>
</Style.Triggers>
</Style>
<CheckBox Content="Checkbox with based on multitrigger" Style="{StaticResource ResourceKey=basedOnMultiStyleWithTrigger}"/>
In combination with the above discussion about the ordering of Trigger
s, this also can lead to unexpected results or subtle bugs. Because BasedOn
results in a merger of the Trigger
s, there is effectively also an ordering of the merged triggers and thus the above discussion holds. The above example with the MultiTrigger
results in a merged definition equivalent with the failing ordering example (see the Style
with key x:Key = "expliciteMergeStyleWithSomeTriggersWrong"
).
Ok, you should have a feeling for how these Trigger
s work by now
So far, we've not been paying any attention as to the kind of properties we can monitor for changes. After all, our trigger must somehow get notified of the changes to the property. It will be no surprise that regular properties will not be sufficient to act as sources of Trigger
s. However, the Trigger
also does NOT support properties backed by INotifyPropertyChanged
notification
Following will thus have no result:
public class MyCustomButton : Button, INotifyPropertyChanged
{
bool mNotifyChangesProperty;
public bool NotifyChangesProperty
{
get { return mNotifyChangesProperty; }
set
{
mNotifyChangesProperty = value;
OnPropertyChanged("NotifyChangesProperty");
}
}
public bool MuteProperty
{
get;
set;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
<Style x:Key="triggerFromMuteProperty">
<Setter Property="Control.Background" Value="Green" />
<Style.Triggers>
<Trigger Property="me:MyCustomButton.MuteProperty" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="triggerFromNotifyChangedProperty">
<Setter Property="Control.Background" Value="Green" />
<Style.Triggers>
<Trigger Property="me:MyCustomButton.NotifyChangesProperty" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
<me:MyCustomButton x:Name="btnMuteProperty" Content="Button with regular property: failed binding" Style="{StaticResource ResourceKey=triggerFromMuteProperty}" Click="MutePropertyOnClickHandler"/>
<me:MyCustomButton x:Name="btnNotifyChangesProperty" Content="Button with INotifyPropertyChanged property: failed binding" Style="{StaticResource ResourceKey=triggerFromNotifyChangedProperty}" Click="NotifyChangedPropertyOnClickHandler"/>
The above sections are commented in the sourcecode, just uncomment them to see what happens if you do try this.
OK, so what is the scope of our trigger? Can we get to properties of other elements? Can we get to properties of the types of our properties? Let's find out.
First, let's try to get at some other element: the Trigger
s Setter
has an attribute called TargetName
which looks promissing. Unfortunately, it is not useable outside templates. According to the MSDN documentation (and also Stackoverflow):
You can set this property to the name of any element within the scope of where the setter collection (the collection that this setter is part of) is applied. This is typically a named element that is within the template that contains this setter.
While the first part of the sentence might leave an opening for applying this directly to controls, practice quickly gets us dissapointed:
<Style x:Key="styleWithCrossObjectTrigger" TargetType="Control">
<Style.Triggers>
<Trigger Property="CheckBox.IsChecked" Value="True">
<Setter TargetName="txtTarget" Property="Control.Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="styleWithCrossObjectDataTrigger" TargetType="Control">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MuteProperty}" Value="True">
<Setter TargetName="txtTarget" Property="Control.Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
The above sections are commented in the sourcecode, just uncomment them to see what happens if you do try this.
So let's go for the second part:
<Style x:Key="styleWithTargetNameInTemplate" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<StackPanel>
<ContentPresenter Content="{Binding Path=Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}" />
<Label x:Name="ourTarget" Content="Label inside ControlTemplate" Background="Green" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="true">
<Setter TargetName="ourTarget" Property="Background" Value="Red" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Button Content="Button with Template using TargetName" Style="{StaticResource ResourceKey=styleWithTargetNameInTemplate}" />
<Button Content="Other Button with same Template using TargetName" Style="{StaticResource ResourceKey=styleWithTargetNameInTemplate}" />
The above definitions will result in following visuals:
Ok, so much for cross object trigering.
So, we must stay inside the object on which we defined the trigger. But can we instruct WPF to search inside the object of a property?
public class DependencyPropertyDataClass : DependencyObject
{
public static readonly DependencyProperty MyDependencyPropertyProperty =
DependencyProperty.Register("MyDependencyProperty", typeof(bool), typeof(DependencyPropertyDataClass), new PropertyMetadata(false));
public bool MyDependencyProperty
{
get { return (bool)GetValue(MyDependencyPropertyProperty); }
set { SetValue(MyDependencyPropertyProperty, value); }
}
}
public class MyCustomButtonWithDataContext : Button
{
public MyCustomButtonWithDataContext()
{
this.DataContext = new MyCustomButton();
}
public static readonly DependencyProperty TypedDataContextProperty =
DependencyProperty.Register("MyDependencyProperty", typeof(MyCustomButton), typeof(MyCustomButtonWithDataContext), new PropertyMetadata(null));
public MyCustomButton TypedDataContext
{
get { return (MyCustomButton)GetValue(TypedDataContextProperty); }
set { SetValue(TypedDataContextProperty, value); }
}
}
Next, we try to define a trigger on this nested property:
<Style x:Key="styleWithTriggerFromNestedProperty">
<Style.Triggers>
<Trigger Property="me:MyCustomButtonWithDataContext.TypedDataContext.MyDependencyProperty" Value="True">
<Setter Property="Control.Foreground" Value="Green" />
</Trigger>
</Style.Triggers>
</Style>
This does not work: we get a compilation error
I've been doubting if I should bother with the following case now allready or should wait till the next kind of trigger. Anyway, I decided to draw your attention to this now: what is the scope of the Trigger
? Well, the scope is the Control
on which it is applied and NOTHING else. It does not check any containing controls, neither does it search for properties in the DataContext
.
As a result, following will do nothing:
btnPropTriggerNotifyChangesProperty.DataContext = new CheckBox();
private void PropTriggerNotifyChangedPropertyOnClickHandler(object sender, RoutedEventArgs e)
{
(btnPropTriggerNotifyChangesProperty.DataContext as CheckBox).IsChecked = !(btnPropTriggerNotifyChangesProperty.DataContext as CheckBox).IsChecked;
}
<Style x:Key="propertyTriggerFromNotifyChangedProperty">
<Style.Triggers>
<Trigger Property="CheckBox.IsChecked" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
<Button x:Name="btnPropTriggerNotifyChangesProperty" Content="Button with INotifyPropertyChanged property using propertytrigger" Style="{StaticResource ResourceKey=propertyTriggerFromNotifyChangedProperty}" Click="PropTriggerNotifyChangedPropertyOnClickHandler"/>
We've set the DataContext
of the Button
to a CheckBox
and define a Trigger
to monitor for changes in the IsChecked
state of the CheckBox
. In the click handler of the Button
, we toggle the IsChecked
porperty of the DataContext
. You migh thave expected the Trigger
to fire but it doesn't: the Trigger
does not look in the Button
s DataContext
.
The above definitions will result in following visuals:
Which makes for a nice transition to the following type of Trigger
: the DataTrigger
.
Data Triggers
Concepts
Why do we need DataTrigger
s? I've allready given you a hint at the end of the final paragraph in the discussion on Trigger
s: with regular Trigger
s we can only monitor changes in properties of the control on which the Trigger
is applied. We can not trigger on nested properties, or on changes in the DataContext
.
Also, by using DataTriggers, we can fire our trigger when INotifyPropertyChanged backed properties change.
The main difference between a regular Trigger
and a DataTrigger
is that the source of the trigger is not specified simply by the name of a property, but through a Binding
object. It is by using a Binding
that we now can use INotifyPropertyChanged
backed properties, specify another object as the source of the trigger, etc...
Let's find out how to do all this.
How to do it?
DataTrigger
s, when using the simplest definition, operate as their name suggests on the DataContext
of the Control
they are defined on.
Let's try this out:
<Style x:Key="styleWithDataTriggerUsingSetter">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsChecked}" Value="True">
<Setter Property="CheckBox.Foreground" Value="Green"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<me:MyCustomCheckBox x:Name="chkWithDataContext" Content="Press any of the below buttons" Style="{StaticResource ResourceKey=styleWithDataTriggerUsingSetter}" />
<Button Content="Click me to check the above CheckBox" Click="CheckCheckBox"/>
<Button Content="Click me to check the DataContext object of the above CheckBox" Click="CheckCheckBoxDataContext"/>
When clicking the first button nothing happens and this is to be expected: as stated above, the DataTrigger
operates on the DataContext
and as such, setting the IsChecked
property on the CheckBox
itself does NOT fire the DataTrigger
.
The second Button
however does change the IsChecked
property of the DataContext
and thus also fires the DataTrigger
.
So, above definitions lead to following visuals:
And just as we can provide EnterActions
and ExitActions
instead of Setter
s on a regular Trigger
, we can do the same on DataTrigger
s.
There are of course times when you will want to bind to a property of the control itself. The Binding
class provides a bunch of ways to specify the source of the binding. For demonstration purposes, I've provided the below example which provides a Binding
to the Control
itself:
<Style x:Key="styleWithDataTriggerUsingSetterBindToControl">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsChecked}" Value="True">
<Setter Property="CheckBox.Foreground" Value="Green"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<me:MyCustomCheckBox x:Name="chkWithDataContextBindToControl" Content="Press the below button" Style="{StaticResource ResourceKey=styleWithDataTriggerUsingSetterBindToControl}" />
<Button Content="Click me to check the above CheckBox with a Control binding" Click="CheckCheckBoxWithControlBinding"/>
private void CheckCheckBoxWithControlBinding(object sender, RoutedEventArgs e)
{
chkWithDataContextBindToControl.IsChecked = !chkWithDataContextBindToControl.IsChecked;
}
Above definitions lead to following visuals:
As mentioned above, the Binding
class has several possibilites for specifying its source. I will not mention them all here as not to clutter this article with information, allthough valueable, not specific to DataTrigger
s but specific to Binding
s. I intend to provide a similar discussion as I'm doing on Trigger
s, but then on Binding
in a future post.
Similar to the MultiTrigger
, we have the MultiDataTrigger
. It is defined in a similar fashion: you specify multiple Condition
s which must all be fullfilled before the trigger is fired:
<Style x:Key="styleWithMultiDataTrigger">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=IsChecked}" Value="True" />
<Condition Binding="{Binding Path=PropertyWithOtherName}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="CheckBox.Foreground" Value="Green"></Setter>
</MultiDataTrigger>
</Style.Triggers>
</Style>
<me:MyCustomCheckBox x:Name="chkWithMultiDataContext" Content="Press any of the below buttons" Style="{StaticResource ResourceKey=styleWithMultiDataTrigger}" />
<Button Content="MultiData: Click me to toggle the IsChecked of the DataContext object of the above CheckBox" Click="ToggleIsCheckCheckBoxDataContext1"/>
<Button Content="MultiData: Click me to toggle the PropertyWithOtherName of the DataContext object of the above CheckBox" Click="TogglePropertyWithOtherNameCheckBoxDataContext1"/>
It will be no surprise that the trigger only fires when both Condition
s are met.
So, above definitions lead to following visuals:
There is an alternative to this MultiDataTrigger
: the MultiBinding
. With a MultiBinding
, we can bind to multiple properties at once. However, the problem then is: how do we specify the value on which we want our DataTrigger
to fire? After all, we have multiple values now, but we can only specify a single value in our DataTrigger
. The solution is to use a class which implements the IMultiValueConverter
interface. The single values from your MultiBinding
are handed to your converter and then you can merge them into a single return value:
<me:MyCustomMultiValueConverter x:Key="myConverter"/>
<Style x:Key="styleWithMultiBindingDataTrigger">
<Style.Triggers>
<DataTrigger Value="V1:True;V2:True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource myConverter}">
<Binding Path="IsChecked" />
<Binding Path="PropertyWithOtherName" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="CheckBox.Foreground" Value="Green"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<me:MyCustomCheckBox x:Name="chkWithMultiBindingDataContext" Content="Press any of the below buttons" Style="{StaticResource ResourceKey=styleWithMultiBindingDataTrigger}" />
<Button Content="MultiBinding: Click me to toggle the IsChecked of the DataContext object of the above CheckBox" Click="ToggleIsCheckCheckBoxDataContext2"/>
<Button Content="MultiBinding: Click me to toggle the PropertyWithOtherName of the DataContext object of the above CheckBox" Click="TogglePropertyWithOtherNameCheckBoxDataContext2"/>
The code of the valueconverter looks like following:
public class MyCustomMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
return "V1:" + values[0].ToString() + ";V2:" + values[1].ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("Cannot convert back");
}
}
So, above definitions lead to following visuals:
I think it will be clear from the above discussion that the solution with the MultiBinding
is less than ideal: in all but the most simple case, this will require writing a custom class implementing the IMultiValueConverter
interface, where the use of a MultiDataTrigger
just works out of the box.
You may not have noticed from the above discussion, but in the last samples we have been binding on INotifyPropertyChanged
backed properties. The DataTrigger
allows notification from Dependency properties as the regular Trigger
, but also from INotifyPropertyChanged
backed properties. But how exactly do these last work? Let's find out.
The canonical form is pretty straight forward:
public class NotifyPropertyChangedDataClass : INotifyPropertyChanged
{
bool mPropertyWithName;
public bool PropertyWithName
{
get { return mPropertyWithName; }
set
{
mPropertyWithName = value;
OnPropertyChanged("PropertyWithName");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private void SwitchByPropertyName(object sender, RoutedEventArgs e)
{
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithName = !(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithName;
}
<Style x:Key="styleWithDataTriggerBindByPropertyName">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PropertyWithName}" Value="True">
<Setter Property="CheckBox.Foreground" Value="Green"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<CheckBox x:Name="chkWithDataContextPropertyWithName" Content="Press the below button" Style="{StaticResource ResourceKey=styleWithDataTriggerBindByPropertyName}" />
<Button Content="Click me to check the topmost CheckBox using the actual propertyname" Click="SwitchByPropertyName"/>
The binding registers event PropertyChangedEventHandler PropertyChanged
to be notified of any changes in properties and by the provided name in the PropertyChangedEventArgs
argument knows which property to check.
Above definitions lead to following visuals:
It is of course still not possible to have regular properties as sources for the DataTrigger
:
private void DataContextMutePropertyOnClickHandler(object sender, RoutedEventArgs e)
{
(btnDataContextNotifyChangesProperty.DataContext as MyCustomButton).MuteProperty = !(btnDataContextNotifyChangesProperty.DataContext as MyCustomButton).MuteProperty;
}
<Style x:Key="dataTriggerFromMuteProperty">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MuteProperty}" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
<me:MyCustomButtonWithDataContext x:Name="btnDataContextMuteProperty" Content="Button with DataContext regular property" Style="{StaticResource ResourceKey=dataTriggerFromMuteProperty}" Click="DataContextMutePropertyOnClickHandler"/>
Next are some permutations on the used propertyname, to see how things work. They are somewhat theoretical in nature, so you're free to skip forward.
What happens if we bind a non existing property, but also use this non-existing propertyname in the event PropertyChangedEventHandler PropertyChanged
?
public class NotifyPropertyChangedDataClass : INotifyPropertyChanged
{
public bool UseCorrectPropertyName
{
get;
set;
}
public bool UseOtherPropertyName
{
get;
set;
}
bool mPropertyWithName;
public bool PropertyWithName
{
get { return mPropertyWithName; }
set
{
mPropertyWithName = value;
OnPropertyChanged(UseOtherPropertyName?
"PropertyWithOtherName"
:(UseCorrectPropertyName ? "PropertyWithName" : "PropertyWithWrongName"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private void SwitchByPropertyWrongName(object sender, RoutedEventArgs e)
{
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).UseOtherPropertyName = false;
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).UseCorrectPropertyName = false;
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithName = !(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithName;
}
<Style x:Key="styleWithDataTriggerBindByNotificationName">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PropertyWithWrongName}" Value="True">
<Setter Property="CheckBox.Foreground" Value="Blue"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<CheckBox x:Name="chkWithDataContextOtherPropertyWithWrongName" Content="Press the below button" Style="{StaticResource ResourceKey=styleWithDataTriggerBindByNotificationName}" />
<Button Content="Click me to check the topmost CheckBox using the incorrect propertyname" Click="SwitchByPropertyWrongName"/>
Well, you can't fool .NET this easily: nothing is happening. Apparently, it looks like the Binding
checks to see if the propertyname used effecively exists. Thus, you cannot make phantom properties. And because the propertyname used in the above example does not exist, nothing happens when we click the button. It is logical because of the property does not exist, how is the Binding
to know the final value?
Another possible permutation is to use in one property, the name of another property
private void SwitchWronglyByPropertyOtherName(object sender, RoutedEventArgs e)
{
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).UseOtherPropertyName = true;
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).UseCorrectPropertyName = false;
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithName = !(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithName;
}
private void SwitchCorrectByPropertyOtherName(object sender, RoutedEventArgs e)
{
(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithOtherName = !(chkWithDataContextPropertyWithName.DataContext as NotifyPropertyChangedDataClass).PropertyWithOtherName;
}
<Style x:Key="styleWithDataTriggerBindByNotificationName">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PropertyWithWrongName}" Value="True">
<Setter Property="CheckBox.Foreground" Value="Blue"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="styleWithDataTriggerBindByOtherName">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PropertyWithOtherName}" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
<CheckBox x:Name="chkWithDataContextOtherPropertyWithWrongName" Content="Press the below button" Style="{StaticResource ResourceKey=styleWithDataTriggerBindByNotificationName}" />
<CheckBox x:Name="chkWithDataContextOtherPropertyWithName" Content="Press the below button" Style="{StaticResource ResourceKey=styleWithDataTriggerBindByOtherName}" />
<Button Content="Click me to check the middle/bottom CheckBox using the wrongly used other propertyname" Click="SwitchWronglyByPropertyOtherName"/>
<Button Content="Click me to check the bottom CheckBox using the correctly used other propertyname" Click="SwitchCorrectByPropertyOtherName"/>
Again, nothing has changed. Allthough you specify the name of the PropertyWithOtherName
property in the notification, the actual value of it didn't change. So WPF asks the value of the property, sees that it is still false
and does nothing. Of course, using the correct name will fire our DataTrigger
as is done with the last button.
What is the scope of the DataTriger
and how can we reference other objects as sources, if possible at all?
The discussion on the TargetName
property of a Setter
still holds: you cannot use it in regular <codestyle< code="">s, but only in <code>Templates:
<Style x:Key="styleWithCrossObjectDataTrigger" TargetType="Control">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MuteProperty}" Value="True">
<Setter TargetName="txtTarget" Property="Control.Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
Fortunately, the Binding
class has a much richer interface for defining its source then the simple Property
property of a regular Trigger
. As allready stated above, the Binding
class itself deserves an article by itself, which I intend to write. But for the discussion at hand I will show you the simplest syntax for referencing another object by its name:
<Style x:Key="styleWithCrossObjectDataTrigger">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=chkTarget, Path=IsChecked}" Value="True">
<Setter Property="Control.Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
<TextBox x:Name="txtTarget" Text="Textbox with trigger source from checkbox" Style="{StaticResource ResourceKey=styleWithCrossObjectDataTrigger}" />
<CheckBox x:Name="chkTarget" Content="Checkbox wich is the trigger source" >
</CheckBox>
Mind that the Binding
references the source of the trigger. Thus, the Style
defining the target property to change, must be defined on the object having that property. Or put another way: the Style
defining the trigger must be applied on the target of the trigger and NOT the source.
Above definitions lead to following visuals:
Event Triggers
Concepts
In the above code we where able to take some actions or set a property when for example the mouse is over a control because many controls provide something like conveniance properties which change their value when a certain event happens. But what if there is no such property?
Enter EventTrigger
s
How to do it?
EventTrigger
s allow you to take an action when an event happens.
Following is an example of its use:
<Style x:Key="styleWithEventTrigger" TargetType="TextBlock">
<Style.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)" To="Green" Duration="0:0:0.25" >
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
<TextBlock Text="TextBlock with eventtrigger" Background="Red" Style="{StaticResource ResourceKey=styleWithEventTrigger}" />
Above definitions lead to following visuals:
Now, read the above sentence once again please. Yes, you can take a TriggerAction
but you cannot simply set a property as with the other type of Trigger
s. If you look around on the internet however you can find workarounds. Thus, following will not work:
Just as you can use multiple regular Trigger
, you can also use multiple EventTrigger
s:
<Style x:Key="styleWithMultipleEventTriggers" TargetType="TextBox">
<Style.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)" To="Green" Duration="0:0:0.25" >
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="GotFocus">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)" To="Blue" Duration="0:0:0.25" >
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
<TextBox Text="TextBox with multiple eventtriggers" Background="Red" Style="{StaticResource ResourceKey=styleWithMultipleEventTriggers}" />
Above definitions lead to following visuals:
There is something to be aware of here: the actions are triggered by events and their result remains active. After all, there is no undo of an event. A property can be set to a certain value and then be kind of unset by setting it to another value. An event just happens. That is why in the above definition, once you hover over the control, and by doing so have triggered the MouseEnter
event, the Background
color will change and remain like that, until another event changes it, like in the above the GotFocus
event.
There is no such thing as a MultiEventTrigger
. If you think about this its is logical: after all events will always / most of the time happen in a serial manner and not exactly at the same time. And how would you then define "multi"? Two events happening in succession? But how do you define "succession"? Take the above example: MouseEnter and GotFocus. There will have happened MouseMoves inbetween. And what if I cross the control: MouseEnter, MouseMove and MouseLeave and then set the Focus in code. Is this still in succession?
What are the type of events you can use?
Just as the other types of triggers where restricted on the kind of properties they could monitor, the EventTrigger
can only monitor RoutedEvent
s. Thus, following will not work:
<Style x:Key="styleWithEventTriggerNonRoutedEvent" TargetType="TextBlock">
<Style.Triggers>
<EventTrigger RoutedEvent="IsEnabledChanged">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)" To="Green" Duration="0:0:0.25" >
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
A somewhat special case: the ItemsControl
Concepts
For those who have read the first part in this series of articles, then this probably will not be such a special case. Most of what has been said there is also valid here. That is because the rules used to select and apply a Style
are the same. It is only what is defined inside the Style
that has changed.
For the others: some controls do have multiple styling properties. An example of these are the ItemsControl
derived controls: they have on top of their regular Style
also a ItemContainerStyle
property. But don't be fooled here: this style assigned to this property is applied to the ItemsContainer and ONLY to the ItemsContainer and NOT to any of it's created children.
How to do it?
Let's play dumb and try the following:
<Style x:Key="myItemsStyle">
<Setter Property="TextBlock.Background" Value="#FF00C1FF" />
<Setter Property="Control.Foreground" Value="Red" />
<Style.Triggers>
<Trigger Property="Control.IsMouseOver" Value="True">
<Setter Property="TextBlock.Background" Value="Red" />
<Setter Property="Control.Foreground" Value="#FF00C1FF" />
</Trigger>
</Style.Triggers>
</Style>
<ItemsControl Grid.Row="0" ItemContainerStyle="{StaticResource myItemsStyle}" >
<ItemsControl.ItemsSource>
<Int32Collection >1,2,3,4,5</Int32Collection>
</ItemsControl.ItemsSource>
</ItemsControl>
If you try the above example you will notice that nothing is happening. The reason is in the comments of the xml.
But, because no itemcontainers are created when using Control
s in the ItemsSource
, following does have the desired effact, allthough we are using the same Style
:
<Style x:Key="myItemsStyle">
<Setter Property="TextBlock.Background" Value="#FF00C1FF" />
<Setter Property="Control.Foreground" Value="Red" />
<Style.Triggers>
<Trigger Property="Control.IsMouseOver" Value="True">
<Setter Property="TextBlock.Background" Value="Red" />
<Setter Property="Control.Foreground" Value="#FF00C1FF" />
</Trigger>
</Style.Triggers>
</Style>
<ItemsControl Grid.Row="1" ItemContainerStyle="{StaticResource myItemsStyle}" >
<TextBlock Text="A textBlock" />
<TextBlock Text="A textBlock" />
<TextBlock Text="A textBlock" />
</ItemsControl>
Above definitions lead to following visuals:
Following also does work. For an explanation I suggest you take a look at the first article.
<Style x:Key="myTextBlockStyle2">
<Style.Resources>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</Style.Resources>
</Style>
<Style x:Key="myListBoxItemsStyle">
<Setter Property="Control.Background" Value="Red" />
<Style.Triggers>
<Trigger Property="Control.IsMouseOver" Value="True">
<Setter Property="Control.Foreground" Value="Blue" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="myItemContainerStyle" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<ContentPresenter />
<Label x:Name="ourTarget" Content="Label inside ControlTemplate" Background="Green" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="ourTarget" Property="Background" Value="Red" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ItemsControl Grid.Row="2" ItemContainerStyle="{StaticResource myTextBlockStyle2}" >
<ItemsControl.ItemsSource>
<Int32Collection>10,20,30,40,50</Int32Collection>
</ItemsControl.ItemsSource>
</ItemsControl>
<ListBox Grid.Row="3" ItemContainerStyle="{StaticResource myListBoxItemsStyle}">
<ItemsControl.ItemsSource>
<Int32Collection>100,200,300,400,500</Int32Collection>
</ItemsControl.ItemsSource>
</ListBox>
<ListBox Grid.Row="4" ItemContainerStyle="{StaticResource myItemContainerStyle}" >
<ItemsControl.ItemsSource>
<Int32Collection>101,201,301,401,501</Int32Collection>
</ItemsControl.ItemsSource>
</ListBox>
Above definitions lead to following visuals:
Conclusion
That's it for triggers. I hope you got something from it, I know I have in writing this article. Next will be on Templates, but possible first an intermezzo on Resources and Bindings.
Version history
- Version 1.0: Initial version
- Version 1.1: Following changes:
- Added reference to other articles the series
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.