VisualStateManager and generated transitions

cantloginfromwork picture cantloginfromwork · Jan 28, 2011 · Viewed 7.8k times · Source

Just when I think I understand the VisualStateManager, something proves me wrong.

I'm using WPF 4 and am trying to simply enlarge an item on mouse over, and shrink it back on mouse leave. I figured I'd just define each state in a VisualStateGroup and then specify a VisualTransition with a GeneratedDuration:

<Border x:Name="PART_Root" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" RenderTransformOrigin="0.5,0.5">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup Name="CommonStates">
            <VisualStateGroup.Transitions>
                <VisualTransition GeneratedDuration="0:0:1"/>
            </VisualStateGroup.Transitions>

            <VisualState Name="Normal"/>

            <VisualState Name="MouseOver">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="scaleTransform" Storyboard.TargetProperty="ScaleX" To="1.5" Duration="0"/>
                    <DoubleAnimation Storyboard.TargetName="scaleTransform" Storyboard.TargetProperty="ScaleY" To="1.5" Duration="0"/>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <Border.RenderTransform>
        <ScaleTransform x:Name="scaleTransform" ScaleX="1" ScaleY="1"/>
    </Border.RenderTransform>

    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>

Since I have a catch-all VisualTransition with a GeneratedDuration, I was expecting the VSM to generate intermediate animations. That is, mousing over the control should animate the ScaleTransform properties from 1 to 1.5 over the course of 1 second. Same with mousing off. Instead, there's a delay of 1 second and then the ScaleTransform properties instantly snap to 1.5 or back to 1.

If I manually specify transitions as follows then I get the desired behavior:

<VisualStateGroup.Transitions>
    <VisualTransition From="Normal" To="MouseOver">
        <Storyboard>
            <DoubleAnimation Storyboard.TargetName="scaleTransform" Storyboard.TargetProperty="ScaleX" To="1.5" Duration="{StaticResource MouseEnterDuration}"/>
            <DoubleAnimation Storyboard.TargetName="scaleTransform" Storyboard.TargetProperty="ScaleY" To="1.5" Duration="{StaticResource MouseEnterDuration}"/>
        </Storyboard>
    </VisualTransition>

    <VisualTransition From="MouseOver" To="Normal">
        <Storyboard>
            <DoubleAnimation Storyboard.TargetName="scaleTransform" Storyboard.TargetProperty="ScaleX" To="1" Duration="{StaticResource MouseLeaveDuration}"/>
            <DoubleAnimation Storyboard.TargetName="scaleTransform" Storyboard.TargetProperty="ScaleY" To="1" Duration="{StaticResource MouseLeaveDuration}"/>
        </Storyboard>
    </VisualTransition>
</VisualStateGroup.Transitions>

But why do I have to do this? I thought the whole point of generated transitions was that the transition would be, you know, generated. What am I misunderstanding here?

UPDATE: As per Rick's answer, Blend generates something that does work. Thus, working backwards I determined that it is indeed the fact that I'm referencing the ScaleTransform directly rather than through the UIElement that contains it. I changed my XAML to the following and it works as expected:

<Border x:Name="PART_Root" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" RenderTransformOrigin="0.5,0.5">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup Name="CommonStates">
            <VisualStateGroup.Transitions>
                <VisualTransition From="Normal" To="MouseOver" GeneratedDuration="{StaticResource MouseEnterDuration}"/>

                <VisualTransition From="MouseOver" To="Normal" GeneratedDuration="{StaticResource MouseLeaveDuration}"/>
            </VisualStateGroup.Transitions>

            <VisualState Name="Normal"/>

            <VisualState Name="MouseOver">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="PART_Root" Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)" To="{StaticResource MouseOverScale}" Duration="0"/>
                    <DoubleAnimation Storyboard.TargetName="PART_Root" Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" To="{StaticResource MouseOverScale}" Duration="0"/>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <Border.RenderTransform>
        <ScaleTransform/>
    </Border.RenderTransform>

    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>

Seems ridiculous (and an obvious bug), but it works.

Thanks

Answer

Rick Sladkey picture Rick Sladkey · Jan 29, 2011

I can't say that I fully understand why it doesn't work the way we'd expect it to, but in these situations we can use Expression Blend to perform the task and see what markup it produces. I've done that and here's a working example based on your sample:

<Grid>
    <Grid.Resources>
        <Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Border x:Name="PART_Root" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" RenderTransformOrigin="0.5,0.5">
                            <Border.RenderTransform>
                                <TransformGroup>
                                    <ScaleTransform/>
                                    <SkewTransform/>
                                    <RotateTransform/>
                                    <TranslateTransform/>
                                </TransformGroup>
                            </Border.RenderTransform>
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup Name="CommonStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0:0:1"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState Name="Normal"/>
                                    <VisualState x:Name="MouseOver">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)" Storyboard.TargetName="PART_Root">
                                                <EasingDoubleKeyFrame KeyTime="0" Value="2"/>
                                            </DoubleAnimationUsingKeyFrames>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)" Storyboard.TargetName="PART_Root">
                                                <EasingDoubleKeyFrame KeyTime="0" Value="2"/>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Grid.Resources>
    <Button Content="Button" HorizontalAlignment="Center" VerticalAlignment="Center" Width="75" Style="{StaticResource ButtonStyle1}"/>
</Grid>

Although Blend uses a more general transform group, the main difference we can see is that the storyboard targets an element and a property path through that element to the scaling factors.