Search code examples
c#silverlightxamlvisualstatemanager

Using VisualStateManager to start and stop Storyboards


I have an animation that animates a Canvas by turning it 360 degrees indefinitely (it basically spins). What I want is for this animation to start when the control is shown and then stop when the control is hidden. I figured I could tie this in, somehow, to the VisualStateManager. I have seen an example of fading in and out controls here which could work but I just dont know how to use VSM to start and stop the storyboard

<Canvas.Resources>
    <Storyboard x:Name="spinnerBoard">
        <DoubleAnimation
            Storyboard.TargetName="SpinnerRotate"
            Storyboard.TargetProperty="Angle"
            From="0" To="360" Duration="0:0:01.3"
            RepeatBehavior="Forever" />
    </Storyboard>
</Canvas.Resources>

<Canvas.RenderTransform>
    <RotateTransform x:Name="SpinnerRotate" Angle="0" />
</Canvas.RenderTransform>

Example VSM

<VisualState x:Name="Show">
    <Storyboard>
        <!-- Start the story board here -->
    </Storyboard>
</VisualState>
<VisualState x:Name="Hide">
    <Storyboard>
        <!-- Stop the story board here -->
    </Storyboard>
</VisualState>

Solution

  • A global answer of your different questions :
    ExtendedVisualStateManager.GoToElementState returns false in Silverlight
    Default binding to UserControl for custom DP

    You can do something like this :

    1. Use a template control that extend ContentControl to play with IsEnabled of content (prevent action during waiting) ;
    2. Create a DP IsWaiting that switch your control visual state ;
    3. Create the two states in XAML : Use DoubleAnimation with RepeatBehavior="Forever"

    After you can add a overlay and a Waiting message dependency property like the busy indicator control...

    I use a picture for the Waiting visual part but you can use a canvas, grid etc...

    C#

    [TemplateVisualState(GroupName = "WaitGroup", Name = WaitSpinner.IsWaitingStateName)]
    [TemplateVisualState(GroupName = "WaitGroup", Name = WaitSpinner.NotWaitingStateName)]
    public class WaitSpinner : ContentControl
    {
        #region States names
        internal const String IsWaitingStateName = "IsWaitingState";
        internal const String NotWaitingStateName = "NotWaitingState";
        #endregion States names
    
        public bool IsWaiting
        {
            get { return (bool)GetValue(IsWaitingProperty); }
            set { SetValue(IsWaitingProperty, value); }
        }
    
        public static readonly DependencyProperty IsWaitingProperty =
            DependencyProperty.Register("IsWaiting", typeof(bool), typeof(WaitSpinner), new PropertyMetadata(false, OnIsWaitingPropertyChanged));
    
        private static void OnIsWaitingPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            WaitSpinner waitSpinner = (WaitSpinner)sender;
            waitSpinner.ChangeVisualState(true);
        }
    
        public WaitSpinner()
        {
            DefaultStyleKey = typeof(WaitSpinner);
        }
    
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            ChangeVisualState(false);
        }
    
        protected virtual void ChangeVisualState(bool useTransitions)
        {
            VisualStateManager.GoToState(this, IsWaiting ? IsWaitingStateName : NotWaitingStateName, useTransitions);
        }
    }
    

    Xaml :

    <VisualStateGroup x:Name="WaitGroup">
        <VisualState x:Name="NotWaitingState" >
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Control.IsEnabled)" Storyboard.TargetName="content">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <System:Boolean>True</System:Boolean>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="IsWaitingState">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="WaitPart">
                    <DiscreteObjectKeyFrame KeyTime="0:0:0.200" Value="Visible"/>
                </ObjectAnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" Storyboard.TargetName="WaitPart" To="360" RepeatBehavior="Forever" Duration="0:0:1" />
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Control.IsEnabled)" Storyboard.TargetName="content">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <System:Boolean>False</System:Boolean>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
    </VisualStateGroup>
    <!-- ............. -->
    <ContentControl
        IsTabStop="False"
        x:Name="content"
        Content="{TemplateBinding Content}"
        ContentTemplate="{TemplateBinding ContentTemplate}"
        HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
        VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
        Foreground="{TemplateBinding Foreground}"
        ScrollViewer.HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
        ScrollViewer.VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"/>
    <Image Source="CirclePicture.png"
        x:Name="WaitPart"
        RenderTransformOrigin="0.5,0.5"
        Width="16"
        Height="16"
        Visibility="Collapsed"
        IsHitTestVisible="False">
        <Image.RenderTransform>
            <RotateTransform  />
        </Image.RenderTransform>
    </Image>