VisualStateManager

WPF: Animate Your Button’s Enabled/Disabled Event

Posted on Updated on

Probably this sounds really easy. I’ll bet you’re thinking “all you have to do is create a style with an event trigger for the event ‘IsEnabledChanged‘ and plug-in some animation.” Unfortunately, this won’t fly because event triggers only work for routed events, and IsEnabledChanged is not a routed event.

Animation Reduces Confusion for State Changes

Why would you want to animate a button? To call attention to it, so the user realizes they now have the power to do something they didn’t before. Little touches like this can greatly reduce training time for your apps, and generate good will with your users. According to this interview on DotNetRocks, you can reduce the overall cost of your app by 10% – 40% by making it more intuitive this way. (Note that you need to do more than animate just one button!) Back to the correct animation approach. You can’t do it the obvious way, using an event trigger. That leaves you with three approaches:

  1. Write a data trigger
  2. Create your own routed event and raise it in code when the button’s IsEnabledChanged event fires
  3. Create your own ControlTemplate and utilize the VisualStateManager to provide animations

Options one and two have this disadvantage: the animation is not automatic. You have to set some flag in your code to signal to the data trigger that you want the animations to fire, or else raise your custom event. Effectively you would perform two operations in code every time you disable your button (set enabled state, then set the code flag). I like option three, even though it requires more work to set up. After setting up, it is easier to use because the animation happens automatically. Benefit: for big projects, you write the template once and never have to look at  again. You also gain the option to customize your button in other ways, which I won’t discuss here. To use VisualStateManager, you create a style with a ControlTemplate, my research has not uncovered any other way. That requires you to custom-build most of your button’s display features. Here is a screen shot of the enabled and disabled states of my sample:

Screen shot showing Enabled and Disabledabled States
Screen shot showing the enabled and disabled states. Color differences are slightly exaggerated here to highlight the different states.

To mimic the normal button appearance, I used two rectangles, one on above the other, generating the button’s appearance. Examine the left above: the dark gray top of the button, comes from one of my rectangles. The black bottom part comes from my other rectangle. The disabled appearance on the right uses the same two rectangles, but the animation changes the colors to White and WhiteSmoke. I supply a ColorAnimation for each rectangle.

You can’t tell from the screen shot, but I elected to send the colors through 3 states to make it obvious something has happened to the button:

  1. From disabled colors
  2. Through pale yellow colors
  3. To enabled colors

Or vice-versa when changing the other way, normal to disabled.

Here is the style that uses the VisualStateManager:

<Style TargetType="Button" x:Key="AnimatedEnableButtonStyle" >
    <Setter Property="Margin" Value="5" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border BorderBrush="Black" BorderThickness="1" CornerRadius="2" 
                           TextBlock.Foreground="Black" 
                           RenderTransformOrigin=".5,.5" Name="theBorder" >
                    <Border.RenderTransform>
                        <TransformGroup>
                            <ScaleTransform x:Name="scaleTransform" />
                        </TransformGroup>
                    </Border.RenderTransform>
                    <Grid >
                        <Grid.RowDefinitions>
                            <RowDefinition />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <Rectangle Name="topBackground" Fill="WhiteSmoke"  />
                        <Rectangle Grid.Row="1" Name="bottomBackground" 
                                   Fill="LightGray" />
                        <ContentPresenter Grid.RowSpan="2" 
                               VerticalAlignment="Center" 
                               HorizontalAlignment="Center" Margin="5 0" />
                    </Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup Name="CommonStates">
                            <VisualState Name="Disabled">
                                <Storyboard>
                                    <ColorAnimation Storyboard.TargetName="topBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).(Color)"
                                          To="White" Duration="0:0:.5" />
                                    <ColorAnimation Storyboard.TargetName="bottomBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).(Color)"
                                          To="WhiteSmoke" Duration="0:0:0.5" />
                                    <ColorAnimation Storyboard.TargetName="theBorder"
                                          Storyboard.TargetProperty="(TextBlock.Foreground).(Color)"
                                          To="Gray" Duration="0:0:0.5" />
                                </Storyboard>
                            </VisualState>

                            <VisualState Name="Normal">
                                <Storyboard>
                                    <ColorAnimation Storyboard.TargetName="topBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).Color"
                                          To="PeachPuff" Duration="0:0:0.5" />
                                    <ColorAnimation Storyboard.TargetName="topBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).Color"
                                          BeginTime="0:0:0.5"
                                          To="WhiteSmoke" Duration="0:0:0.5" />
                                        
                                    <ColorAnimation Storyboard.TargetName="bottomBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).Color"
                                          To="LightYellow" Duration="0:0:0.5" />
                                    <ColorAnimation Storyboard.TargetName="bottomBackground"
                                          BeginTime="0:0:0.5"
                                          Storyboard.TargetProperty="(Rectangle.Fill).(Color)"
                                          To="LightGray" Duration="0:0:0.5" />
                                        
                                    <ColorAnimation Storyboard.TargetName="theBorder"
                                          Storyboard.TargetProperty="(TextBlock.Foreground).Color"
                                          To="Black" Duration="0:0:0.5" />

                                    <ColorAnimation Storyboard.TargetName="theBorder"
                                          Storyboard.TargetProperty="BorderBrush.Color"
                                          From="Black" To="DarkBlue" 
                                          Duration="0:0:1" AutoReverse="True" />
                                </Storyboard>
                            </VisualState>
                            <VisualState Name="Pressed">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="scaleTransform"
                                          Storyboard.TargetProperty="ScaleX" 
                                          To=".9" Duration="0" />
                                    <DoubleAnimation Storyboard.TargetName="scaleTransform"
                                          Storyboard.TargetProperty="ScaleY"
                                          To=".9" Duration="0" />
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Using the control style is pretty simple, but,  just to make sure, here is a sample showing how you will use it:

<Button Content="Test Button" Name="btnTest" 
            IsEnabled="{Binding ElementName=chkEnabled,Path=IsChecked}" 
            Height="30" Margin="5" 
            Click="btnTest_Click"
            Style="{StaticResource AnimatedStyle}" />

Dissecting the Style

Note that my control template completely replaces the normal button appearance. Should you desire, you can capitalize on this to make custom buttons, such as round buttons. Note that I define my button using

  1. A border, with a name
  2. An empty scale transform, with a name,
  3. A grid
  4. Containing two rectangles, each named.
  5. A content presenter

Tip: provide names (such as for my named border, rectangle and scale transform) to make it easier to specify the target of your animations. As discussed above, my button appearance mimics the standard button, other than the animations. This requires two rectangles, the top and bottom of the button.

My Control States

Note that I have provided animation for three different control states:

  1. Disabled
  2. Normal
  3. Pressed

I’ve looked around for a complete list of states, all I could find was Karen Corby’s Blog post. For normal and disabled states, I provide an ColorAnimation, cycling from dull colors, through pale yellows, to the normal enabled colors. Not that ColorAnimation allows you to specify a BeginTime; that is how you make your colors change through more than one state. For example, you start at the disabled color, use a ColorAnimation to move to a pale yellow, then, start your next animation when the first one ends, using the BeginTime; the effect is one animation starting after the other ends. For the pressed state, I use a scale transform to briefly shrink the button by 10 % (to .9).

Summary

Animations make it apparent to the user that something has changed and reduce training costs, making your app more profitable overall. You use a VisualStateManager inside a ControlTemplate to provide animations for each state (disabled, pressed, normal).