The VisualStateManager and Triggers
Silverlight introduced the Visual State Manager, which makes it easier for control template authors to specify the appearance of a control depending on its visual state. The WPF Toolkit ships a Visual State Manager, and the next version of WPF will include the VSM as well. The introduction of the VisualStateManager has understandably led to questions about when to use the VSM instead of triggers and when triggers are appropriate. This blog post attempts to address that question; more thorough discussions about how to use the VisualStateManager are elsewhere.
The VisualStateManager supports the parts and states model, which is a way for control authors to formalize what visual states should be in a control template. The VisualStateManager enables control authors to manage the states of a control and provides a way for designer tools such as Microsoft Blend to support customizing the control's appearance according to its visual state. Before the parts and states model was introduced, it was common for control template authors to use triggers to change a control's appearance when it changed visual states. The following control template uses triggers to change the button's border color when the mouse is over it or when it is pressed.
<ControlTemplate TargetType="Button">
<Grid>
<Ellipse Fill="{TemplateBinding BorderBrush}"/>
<Ellipse x:Name="ButtonShape" Margin="5" Fill="{TemplateBinding Background}"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="Cyan"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="BorderBrush" Value="Red"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
In this model, there is no formal agreement about what the visual states are, there are just some properties defined on a control that can be used to change the control's appearance. A control that follows the parts and control model proactively communicates its visual states to control template authors. When a control uses the VisualStateManager to change its visual states, it expects that the ControlTemplate uses the VisualStateManager to specify the control's appearance for a given visual state. The control template author can also customize transitions between visual states by using VisualTransitions. VisualTransitions enable control template authors to fine-tune an individual transition by changing the times and durations of individual property changes and even animate new properties not mentioned in either state. The following example uses the VisualStateManager to specify the changes in the control's appearance instead of triggers.
<ControlTemplate TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<!--Take one half second to transition to the MouseOver state.-->
<VisualTransition To="MouseOver" GeneratedDuration="0:0:0.5" />
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="borderColor" Storyboard.TargetProperty="Color" To="Cyan"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="borderColor" Storyboard.TargetProperty="Color" To="Red"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Ellipse>
<Ellipse.Fill>
<SolidColorBrush x:Name="borderColor" Color="Black"/>
</Ellipse.Fill>
</Ellipse>
<Ellipse x:Name="ButtonShape" Margin="5" Fill="{TemplateBinding Background}"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
The control uses the VisualStateManager to change visual states by calling VisualStateManager.GoToState. When that occurs, the VisualStateManager appropriately stops and starts the storyboards in the VisualState and VisualTransition objects so that the button's appearance appropriately changes visual states. Therefore, there is a distinct separation of responsibilities: the control author specifies what the visual states of a control are and determines when a control goes into each visual state; the template author specifies what the control looks like in each visual state.
Triggers aren't dead in WPF, though. You can use triggers to change the appearance of a control for properties that don’t correspond to a visual state. For example, the Button has a IsDefault property, but there is no corresponding visual state on Button. A control template author might want to specify the appearance of the button depending on IsDefault’s value, though, so this is a case of where using a trigger is appropriate. The following example repeats the previous example and adds a trigger to specify the button's appearance depending on the value of IsDefault.
<ControlTemplate TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<!--Take one half second to transition to the MouseOver state.-->
<VisualTransition To="MouseOver" GeneratedDuration="0:0:0.5" />
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="borderColor" Storyboard.TargetProperty="Color" To="Cyan"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="borderColor" Storyboard.TargetProperty="Color" To="Red"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Ellipse>
<Ellipse.Fill>
<SolidColorBrush x:Name="borderColor" Color="Black"/>
</Ellipse.Fill>
</Ellipse>
<Ellipse x:Name="defaultOutline" Stroke="{TemplateBinding Background}" StrokeThickness="2" Margin="2"/>
<Ellipse x:Name="ButtonShape" Margin="5" Fill="{TemplateBinding Background}"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsDefault" Value="False">
<Setter TargetName="defaultOutline" Property="Stroke" Value="Transparent"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
The IsMouseOver and IsPressed properties on Button are still available, but you shouldn’t create triggers against them to change the button's appearance. That doesn't mean that those properties aren't useful, though. Those properties can still be used by application authors to check the control's visual state in code, and control authors should continue to define properties for visual states even when they use the VisualStateManager to transition between their visual states.
Comments
Anonymous
March 01, 2009
Thank you for submitting this cool story - Trackback from DotNetShoutoutAnonymous
March 01, 2009
That was very useful. I'm a little confused about one thing though with regard to the current situation with WPF 3.5 SP1: given that the VisualStateManager is currently just included with the WPF Toolkit, I would've expected that no visual states would be defined for existing controls such as Button - am I missing something? (probably)Anonymous
March 02, 2009
Hi Kevin, You're right, in 3.5 sp1, no controls define visual states. But if you use the toolkit, you can define visual states for some existing controls because the toolkit defines helper classes (called Behaviors) so that those controls can use the VisualStateManager in a control template. From looking at the toolkit's source (http://www.codeplex.com/wpf/Release/ProjectReleases.aspx?ReleaseId=22567), it seems that the following controls can understand VSM. Button ListBoxItem ProgressBar TextBoxBase, ToggleButton You hook up the VSM to the existing control by creating a behavior and setting an attached property on the control so that the VSM knows about the control. For example, if you're creating a template for a Button, you could add the following to your application: <behaviors:ButtonBaseBehavior x:Key="ButtonBaseBehavior"/> <Style TargetType="Button" x:Key="VSMTemplate"> <Setter Property="behaviors:VisualStateBehavior.VisualStateBehavior" Value="{StaticResource ButtonBaseBehavior}" /> <Setter Property="Template"> <!—define the template here--> In the next version of WPF, some controls will have native support for VSM so you will not need to set the VisualStateBehavior property. Hope that helps, CaroleAnonymous
March 02, 2009
DotNetBurner.com - news and articles about .net DotNetBurner