Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

VisualStateManager not working as advertised

The following issue has been plaguing me for days now, but I've only just been able to distill it down to its simplest form. Consider the following XAML:

<Window x:Class="VSMTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <Style TargetType="CheckBox">
            <Setter Property="Margin" Value="3"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="CheckBox">
                        <Grid x:Name="Root">
                            <Grid.Background>
                                <SolidColorBrush x:Name="brush" Color="White"/>
                            </Grid.Background>

                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup Name="CheckStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition To="Checked" GeneratedDuration="00:00:03">
                                            <Storyboard Name="CheckingStoryboard">
                                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
                                                    <DiscreteColorKeyFrame KeyTime="0" Value="LightGreen"/>
                                                </ColorAnimationUsingKeyFrames>
                                            </Storyboard>
                                        </VisualTransition>

                                        <VisualTransition To="Unchecked" GeneratedDuration="00:00:03">
                                            <Storyboard Name="UncheckingStoryboard">
                                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
                                                    <DiscreteColorKeyFrame KeyTime="0" Value="LightSalmon"/>
                                                </ColorAnimationUsingKeyFrames>
                                            </Storyboard>
                                        </VisualTransition>
                                    </VisualStateGroup.Transitions>

                                    <VisualState Name="Checked">
                                        <Storyboard Name="CheckedStoryboard" Duration="0">
                                            <ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
                                                <DiscreteColorKeyFrame KeyTime="0" Value="Green"/>
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>

                                    <VisualState Name="Unchecked">
                                        <Storyboard Name="UncheckedStoryboard" Duration="0">
                                            <ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
                                                <DiscreteColorKeyFrame KeyTime="0" Value="Red"/>
                                            </ColorAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>

                            <ContentPresenter/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <StackPanel>
        <CheckBox x:Name="cb1">Check Box 1</CheckBox>
        <CheckBox x:Name="cb2">Check Box 2</CheckBox>
        <CheckBox x:Name="cb3">Check Box 3</CheckBox>
    </StackPanel>
</Window>

It simply re-templates the CheckBox control so that its background is dependent upon its state:

  • Checked = Green
  • Unchecked = Red
  • Checking (transition) = Light Green
  • Unchecking (transition) = Light Red

So, when you check one of the check boxes, you'd expect it to turn light green for a short period, and then turn green. Similarly, when unchecking, you'd expect it to turn light red for a short period, and then turn red.

And it normally does exactly that. But not always.

Play with the program long enough (I can get it in around 30 seconds) and you'll find that the transition animation sometimes trumps that in the visual state. That is, the check box will continue to appear light green when selected, or light red when unselected. Here's a screenshot illustrating what I mean, taken well after the 3 seconds the transition is configured to take:

enter image description here

When this occurs, it's not because the control didn't successfully transition to the target state. It purports to be in the correct state. I verified this by checking the following in the debugger (for the specific case documented by the above screenshot):

var vsgs = VisualStateManager.GetVisualStateGroups(VisualTreeHelper.GetChild(this.cb2, 0) as FrameworkElement);
var vsg = vsgs[0];
// this is correctly reported as "Unselected"
var currentState = vsg.CurrentState.Name;

If I enable tracing for animations, I get the following output when the transition completes successfully:

System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='6148812'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='8261103'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36205315'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='18626439'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36893403'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckingStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='49590434'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='<null>'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36893403'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckingStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 : 
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='49590434'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='<null>'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='16977025'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='16977025'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='16977025'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 

And I get the following output when the transition fails to complete successfully:

System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='6148812'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='8261103'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36205315'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='18626439'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36893403'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckingStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='49590434'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='<null>'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 : 

The first 12 lines are exactly the same as when the transition succeeds, but the final 10 lines are completely missing!

I've read through all the VSM documentation I could find and have not been able to come up with an explanation for this erratic behavior.

Am I to assume that this is a bug in the VSM? Is there any known explanation or workaround for this issue?

like image 365
Kent Boogaart Avatar asked Feb 15 '11 10:02

Kent Boogaart


2 Answers

I've been able to identify and fix the issue as follows:

Firstly, I downgraded my repro project to .NET 3.5 and grabbed the WPF Toolkit source code from CodePlex. I added the WPF Toolkit project to my solution and added a reference to it from the Repro project.

Next, I ran the app and made sure I could still reproduce the issue. Sure enough, it was easy to do so.

Then I cracked open the VisualStateManager.cs file and started adding some diagnostics in key places that would tell me what code was running and what was not. By adding these diagnostics and comparing the output from a good transition to a bad transition, I was quickly able to identify that the following code was not running when the problem manifested itself:

// Hook up generated Storyboard's Completed event handler
dynamicTransition.Completed += delegate
{
    if (transition.Storyboard == null ||
        transition.ExplicitStoryboardCompleted)
    {
        if (ShouldRunStateStoryboard(control, element, state, group))
        {
            group.StartNewThenStopOld(element, state.Storyboard);
        }

        group.RaiseCurrentStateChanged(element, lastState, state,
                                        control);
    }

    transition.DynamicStoryboardCompleted = true;
};

So the nature of the bug shifted from a problem in VSM to a problem in the Storyboard.Completed event not always being raised. This is an issue I've experienced before and seems to be a source of much angst for any WPF developer doing anything even slightly out of the ordinary when it comes to animations.

Throughout this process I was posting my findings on the WPF Disciples google group, and it was at this point that Pavan Podila responded with this gem:

Kent,

I have had problems in the past for storyboards not firing their completed events. What I have realized is that if you replace a Storyboard directly, without first stopping it, you may see some out-of-order Completed events. In my case I was applying newer Storyboards to the same FrameworkElement, without stopping the earlier Storyboard and that was giving me some issues. Not sure if your case is similar but thought I'll share this tidbit.

Pavan

Armed with this insight, I changed this line in VisualStateManager.cs:

group.StartNewThenStopOld(element, transition.Storyboard, dynamicTransition); 

To this:

var masterStoryboard = new Storyboard();

if (transition.Storyboard != null)
{
    masterStoryboard.Children.Add(transition.Storyboard);
}

masterStoryboard.Children.Add(dynamicTransition);
group.StartNewThenStopOld(element, masterStoryboard);

And - lo and behold - my repro that was previously failing intermittently was now working every time!

So, really this works around a bug or odd behavior in WPF's animation sub-system.

like image 101
Kent Boogaart Avatar answered Oct 21 '22 07:10

Kent Boogaart


It appears as though setting Duration="0" on the Checked and Unchecked storyboards was the culprit. Removing it fixes the problem. I'm not sure I understand why, unless the storyboard is linked to the corresponding transition in some way.

However, I think I found a cleaner solution for you anyway. If you change your ControlTemplate to this then it accomplishes the same thing without the Transitions...

<ControlTemplate TargetType="CheckBox">
  <Grid x:Name="Root">
      <Grid.Background>
          <SolidColorBrush x:Name="brush" Color="White"/>
      </Grid.Background>

      <VisualStateManager.VisualStateGroups>
          <VisualStateGroup Name="CheckStates">
              <VisualState Name="Checked">                                      
                  <Storyboard x:Name="CheckedStoryboard">
                      <ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
                        <DiscreteColorKeyFrame KeyTime="0" Value="LightGreen"/>
                        <DiscreteColorKeyFrame KeyTime="00:00:03" Value="Green"/>
                      </ColorAnimationUsingKeyFrames>
                  </Storyboard>
              </VisualState>

              <VisualState Name="Unchecked">
                  <Storyboard x:Name="UncheckedStoryboard">
                      <ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
                        <DiscreteColorKeyFrame KeyTime="0" Value="LightSalmon"/>
                        <DiscreteColorKeyFrame KeyTime="00:00:03" Value="Red"/>
                      </ColorAnimationUsingKeyFrames>
                  </Storyboard>
              </VisualState>
          </VisualStateGroup>
      </VisualStateManager.VisualStateGroups>

      <ContentPresenter/>
  </Grid>
</ControlTemplate>
like image 2
Steve Wortham Avatar answered Oct 21 '22 05:10

Steve Wortham