silverlightsilverlight-4.0visualstatemanagervisualstategroup

VisualStateGroup Triggering Animation in Another VisualStateGroup


There's something fundamental about VisualStateGroups that I'm not understanding. Everything I've read has led me to believe that they are orthogonal. That is, a state change in one group won't affect other groups. Indeed, they would be rather pointless if this were not the case.

However, in an attempt to understand some odd behavior I was encountering, I put together a simple example that shows that a state change in one group can trigger an animation in another. I'm trying to understand how this can be.

All I want is a ToggleButton-based control to have one appearance when toggled (IsChecked == true) regardless of whether it has focus or whether the mouse is over it, for example.

Firstly, I have a very simple control that transitions between custom states in a custom group:

using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

[TemplateVisualState(Name = normalState, GroupName = activationGroupName)]
[TemplateVisualState(Name = hoverState, GroupName = activationGroupName)]
[TemplateVisualState(Name = activatedState, GroupName = activationGroupName)]
public class CustomControl : ToggleButton
{
    private const string activationGroupName = "Activation";
    private const string normalState = "Normal";
    private const string hoverState = "Hover";
    private const string activatedState = "Activated";

    public CustomControl()
    {
        this.DefaultStyleKey = typeof(CustomControl);

        this.Checked += delegate
        {
            this.UpdateStates();
        };
        this.Unchecked += delegate
        {
            this.UpdateStates();
        };
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        this.UpdateStates();
    }

    protected override void OnMouseEnter(MouseEventArgs e)
    {
        base.OnMouseEnter(e);
        this.UpdateStates();
    }

    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);
        this.UpdateStates();
    }

    private void UpdateStates()
    {
        var state = normalState;

        if (this.IsChecked.HasValue && this.IsChecked.Value)
        {
            state = activatedState;
        }
        else if (this.IsMouseOver)
        {
            state = hoverState;
        }

        VisualStateManager.GoToState(this, state, true);
    }
}

Secondly, I have a template for this control that changes the background color based on the current state:

<Style TargetType="local:CustomControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:CustomControl">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="Activation">
                            <VisualStateGroup.Transitions>
                                <VisualTransition To="Normal" GeneratedDuration="00:00:0.2"/>
                            </VisualStateGroup.Transitions>

                            <VisualState x:Name="Normal"/>

                            <VisualState x:Name="Hover">
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:0.2" To="Red" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
                                </Storyboard>
                            </VisualState>

                            <VisualState x:Name="Activated">
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:0.2" To="Blue" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>

                    <Grid x:Name="grid" Background="White">
                        <TextBlock>ToggleButton that should be white normally, red on hover, and blue when checked</TextBlock>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

For the most part it works, but when the control loses focus it transitions back to the normal state.

I can get around this by explicitly handling focus changes in my control and enacting a state update:

public CustomControl()
{
    this.DefaultStyleKey = typeof(CustomControl);

    this.Checked += delegate
    {
        this.UpdateStates();
    };
    this.Unchecked += delegate
    {
        this.UpdateStates();
    };

    // explicitly handle focus changes
    this.GotFocus += delegate
    {
        this.UpdateStates();
    };
    this.LostFocus += delegate
    {
        this.UpdateStates();
    };
}

However, it makes no sense to me why I have to do this.

Why is a state change in one VisualStateGroup causing an animation defined by another to execute? And what is the simplest, most correct way for me to achieve my stated goal?

Thanks


Solution

  • Note how the GotoState method simply takes the state name without any qualification as to which group it belongs to. Whilst the groups separate the set of states that a control can hold at any one time (one state per group) the state names are expected to be unique across all states in the manager.

    Note also that the ToggleButton from which you derive has the following attribute associated with the class:-

    [TemplateVisualStateAttribute(Name = "Normal", GroupName = "CommonStates")]
    

    This indicates that the logic you have inherit will at some point call GotoState with the state "Normal". Typically controls only have one UpdateStates method (as you have) and at any point where any of the states may have changed it will be called. This function in turn will call GotoState multiple times with one state name from each group. Hence at the point when the control loses focus the ToggleButton calls GotoState to set the "Unfocused" state but along with that will also call it with "Normal" and either "Checked" or "Unchecked".

    Now you've got your own template where the states "Unfocused", "Checked" are not present so those calls to GotoState from ToggleButton don't do anything. However you do have "Normal". The VSM has no way of knowing that the ToggleButton use of "Normal" is intended to choose from its default template's "CommonStates" group. Instead it just finds a VisualState with the name "Normal" which in this case is found in your "Activiation" group. Hence your existing state of "Activated" is replaced with your "Normal".

    Change your "Normal" to "Blah" and things work as you expect.

    (It helps to remember that the group names don't really do a lot despite their presence in the TemplateVisualStateAttribute, their only use I can think of is in the Blend UI)