Let's make the world a more agile place to live. RSS Feed


“Shield” Yourself from Animation Problems in Silverlight Visual State Manager

While working on Vertigo's high definition video player for the Democratic National Convention, our team encountered some challenges with Silverlight 2 Beta 2's Visual State Manager. In this post, I'll discuss how we overcame these challenges, and our solution to make working with VSM simple, quick, and easy. For an introduction to Visual State Manager, check out Scott Guthrie's Silverlight 2 Beta 2 intro here.

Problem #1: Radio Button and ListBoxItem Selection States

Radio buttons and ListBoxItems both have a "selected" state. The XAML for the VisualStateGroups for a ListBoxItem look like this (the same states are used for Radio buttons):

<vsm:VisualStateManager.VisualStateGroups>
     <
vsm:VisualStateGroup x:Name="CommonStates">
          <
vsm:VisualState x:Name="Normal" />
          <
vsm:VisualState x:Name="MouseOver">
               <
Storyboard>
                   
<!-- Animate the MouseOver -->
              
</Storyboard>
          </
vsm:VisualState>
     </
vsm:VisualStateGroup>
     <
vsm:VisualStateGroup x:Name="SelectionStates">
          <
vsm:VisualState x:Name="Selected">
             <
Storyboard>
                    <!-- Animate the Selection -->
            
</Storyboard>
          </
vsm:VisualState>
        <
vsm:VisualState x:Name="Unselected"/>
     </
vsm:VisualStateGroup>
</
vsm:VisualStateManager.VisualStateGroups>

Notice that there are 2 VisualStateGroups – one for "CommonStates", and one for "SelectionStates". Keep in mind that a control must be in one state from each VisualStateGroup at all times. This means that when a ListBoxItem or RadioButton is in the "Selected" state, it must also be in either the Normal/default state or in the MouseOver state:

Possible States
Normal & Unselected
MouseOver & Unselected
Normal & Selected
MouseOver & Selected

This becomes a problem when you need to define a 3-state setup for your buttons, when the same property has different values in all 3 states. If the "Selected" Visual State modifies a property that is also modified by the "MouseOver" Visual State, a conflict occurs.

For example, let's say we want to modify the text color on a ListBox Item or button. When you select the button, it will appear in the "selected" state:

However, when you mouse out of the button's area, the button will return to the "normal" state, even though the button is also in the "selected" state. Since the button is both "selected" and "normal", and both those states modify the text color, the last state to transition "wins".  Your selected button will show both states, with the normal state on top, making the text appear thin and unreadable:

 

Note that the same problem exists for CheckBoxes, which have a "CheckStates" VisualStateGroup.

Problem #2: Animating Brushes

We noticed that when using a ColorAnimation within VSM to change the color of a brush, strange behavior occurs. Either the animation does not occur, or it occurs on an adjacent control. This only happens on 1 out of every 10 transitions - a representative from Microsoft acknowledges the problem in this Silverlight forum discussion.

To see it for yourself, take a look at this sample application built by Page Brooks to illustrate the problem.  Mouse over the dots back and forth - you'll notice states getting stuck. The source code is also available.

Our Solution to Both Problems: Opacity-Only Animations and the "Selected Shield"

To circumvent these problems, we only ever animate opacity. We build a unique element (Grid, Canvas, whatever) for each Visual State, all having an opacity set to 0 other than the element for the "Normal" state. We then simply animate using a DoubleAnimation, changing the opacity on each element.

If our elements include only opaque items, such as images, this would be enough. However, if our elements contain transparent items, we still have a problem. Our selected state will be set to an opacity of 1 along with the MouseOver state, so both will appear to bleed into each other. To solve this, we also include an opaque "Shield" element in the selected state. We set the Z-orders of these elements so that the selected element appears on top of the shield, and the shield element appears on top of the MouseOver and Normal elements. See below for an example of this setup in the XAML.

Of course, having to make multiple copies of the exact same XAML elements is not optimal, and ends up requiring more lines of code. However, it is the only way we have found to totally squash both of these problems.

<ControlTemplate TargetType="ListBoxItem">
     <Grid x:Name="LBGrid" Background="Transparent" Cursor="Hand">

         <
vsm:VisualStateManager.VisualStateGroups>
               <
vsm:VisualStateGroup x:Name="CommonStates">
                    <
vsm:VisualState x:Name="Normal" />
                    <
vsm:VisualState x:Name="MouseOver">
                         <
Storyboard>
                              <
DoubleAnimation Storyboard.TargetName="canvasNormal" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="0"/>
                              <
DoubleAnimation Storyboard.TargetName="canvasMouseOver" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="1"/>
                         </
Storyboard>
                    </
vsm:VisualState>
               </
vsm:VisualStateGroup>
               <
vsm:VisualStateGroup x:Name="SelectionStates">
                    <
vsm:VisualState x:Name="Selected">
                         <
Storyboard>
                              <
DoubleAnimation Storyboard.TargetName="canvasNormal" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="0"/>
                              <
DoubleAnimation Storyboard.TargetName="canvasMouseOver" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="0"/>
                              <
DoubleAnimation Storyboard.TargetName="canvasSelectedShield" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="1"/>
                              <
DoubleAnimation Storyboard.TargetName="canvasSelected" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="1"/>
                         </
Storyboard>
                    </
vsm:VisualState>
               <
vsm:VisualState x:Name="Unselected"/>
          </
vsm:VisualStateGroup>
     </
vsm:VisualStateManager.VisualStateGroups>

     <
Grid.ColumnDefinitions>
          <
ColumnDefinition Width="255"/>
          </
Grid.ColumnDefinitions>
     <
Grid.RowDefinitions>
          <
RowDefinition Height="24"/>
          <
RowDefinition Height="3"/>
     </
Grid.RowDefinitions>
   

     <Canvas x:Name="canvasMouseOver" Grid.Column="0" Grid.Row="0" Opacity="0">
          <
Rectangle Canvas.Left="0" Canvas.Top="0" Stretch="Fill" VerticalAlignment="Stretch" Width="255" Height="24" >

          <Rectangle.Fill>
               <LinearGradientBrush StartPoint="0.511514,-0.00480887" EndPoint="0.511514,1.0625"> 
               <LinearGradientBrush.GradientStops>
                    <GradientStop Color="#80FFFFFF" Offset="0"/>
                    <GradientStop Color="#805B7F9C" Offset="0.34322"/> 
                    <GradientStop Color="#80FFFFFF" Offset="1"/> 
               </LinearGradientBrush.GradientStops>
              
</LinearGradientBrush
          </Rectangle.Fill>
          </Rectangle>
     </
Canvas>

     <Canvas x:Name="canvasNormal" Grid.Column="0" Grid.Row="0" Background="Transparent" Opacity="1">
          <
Rectangle />
     </
Canvas>

     <
Canvas x:Name="canvasSelectedShield" Grid.Column="0" Grid.Row="0" Background="#ABBDD1" Opacity="0"/>

     <
Canvas x:Name="canvasSelected" Grid.Column="0" Grid.Row="0" Opacity="0">
          <Rectangle Canvas.Left="0" Canvas.Top="0" Stretch="Fill" VerticalAlignment="Stretch" Width="255" Height="24" >
           <
Rectangle.Fill>
               <
LinearGradientBrush StartPoint="0.525718,0.0128409" EndPoint="0.525718,1.0513">
                    <
LinearGradientBrush.GradientStops>
                         <
GradientStop Color="#80FFFFFF" Offset="0"/>
                         <
GradientStop Color="#807E9EB8" Offset="0.360731"/>
                         <
GradientStop Color="#80FDFDFD" Offset="1"/>
                    </
LinearGradientBrush.GradientStops>
               </
LinearGradientBrush>
          </
Rectangle.Fill>
          </
Rectangle>
     </
Canvas>

</Grid>
</ControlTemplate>

 
Posted by Bob Cowherd | 2 Comments | Trackback Url | Bookmark with:        
Tags: Work

Links to this Post

Comments

Wednesday, 3 Sep 2008 09:18 by Petar
Nice post.

Wednesday, 3 Sep 2008 10:31 by Morten
I solved this problem differently. Instead of having multiple states, I have two framework elements (parts). One element for selected and one normal, and they share the same two states. This is similar to what the button does (See the parts and states model: http://scorbs.com/2008/06/11/parts-states-model-with-visualstatemanager-part-1-of) The trick is that when you select you disable/enable the two parts. Usually you would want the same thing to happen when you hover on the element regardless of the selection state (change opacity, scale etc). The neat thing I then did was not requiring the two parts to be present. That means that if I don't give the expected ID to the normal part, it won't be disabled and the selected part will be added on to the the existing (depending on your template it could be a new background, an extra overlay etc.)

Name:
URL:
Email:
Comments:

CAPTCHA Image Validation