Using VisualTransition with a Silverlight Content Control

Using VisualTransition with a Silverlight Content Control

  • Comments 13
Note: The zip archive below has been updated after Silverlight 2 released to the web. For more details on the changes I made, see this post.

This post is part of series that demonstrates how to write a Silverlight 2 content control. The first post in the series is here. This post mostly improved on the code and concepts covered in the previous post. The latest code for the content control we’re building can be found here:

Those of you who tried out the CollapsiblePanel control might have noticed something. When the panel first renders on screen, it actually takes two clicks to collapse it back in. I noticed this issue rather late and after some investigation realized what was going on. The IsExpanded flag, which is a boolean, is set to false by default. Clicking the panel the first time it renders sets it to true. However, since the panel renders expanded there is no visual change to be observed. This is why it looks like the panel silently ignores the first click.

I set out to fix the issue by ensuring that the initial value of IsExpanded is respected. Obviously, if a value isn’t provided (as is the case with the sample that accompanies the panel) the panel must start out collapsed. To begin with, I changed the _content_SizeChanged method from this:


void _content_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            FrameworkElement content = sender as FrameworkElement;
            if (content != null)
            {
                TransformGroup tGroup = new TransformGroup();
                TranslateTransform translate = new TranslateTransform();
                translate.SetValue(FrameworkElement.NameProperty, "RollTransform" + Guid.NewGuid().ToString());
                translate.Y = 0;

                tGroup.Children.Add(translate);
                content.RenderTransform = tGroup;

                _RollUpStoryboardName = "RollUp" + Guid.NewGuid().ToString();
                _RollDownStoryboardName = "RollDown" + Guid.NewGuid().ToString();

                _SetupYTranslationStoryboard(translate, _RollUpStoryboardName, -content.ActualHeight);
                _SetupYTranslationStoryboard(translate, _RollDownStoryboardName, 0);
            }
        }

To this:

void _content_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            FrameworkElement content = sender as FrameworkElement;
            if (content != null)
            {
                TransformGroup tGroup = new TransformGroup();
                TranslateTransform translate = new TranslateTransform();
                translate.SetValue(FrameworkElement.NameProperty, "RollTransform" + Guid.NewGuid().ToString());

                if (IsExpanded)
                {
                    translate.Y = 0;
                }
                else
                {
                    translate.Y = -content.ActualHeight;
                }

                tGroup.Children.Add(translate);
                content.RenderTransform = tGroup;

                _RollUpStoryboardName = "RollUp" + Guid.NewGuid().ToString();
                _RollDownStoryboardName = "RollDown" + Guid.NewGuid().ToString();

                _SetupYTranslationStoryboard(translate, _RollUpStoryboardName, -content.ActualHeight);
                _SetupYTranslationStoryboard(translate, _RollDownStoryboardName, 0);
            }
        }

The _content_SizeChanged event is called whenever the size of the content changes. Setting up the translation to –content.ActualHeight when IsExpanded is false means that the content is hidden when the panel is first shown. This is exactly what we expected. When this code is run, the panel we see on screen looks like this:

Collapsed panel

Notice something interesting? We took care of the content, but the title animation, which is defined by a visual state still needs handling. Easy enough, I hear you say :). We’ll just use the Visual State Manager to set the state to Collapsed. Okay then, let’s see what the code looks like when we do that:

if (IsExpanded)
                {
                    translate.Y = 0;
                    VisualStateManager.GoToState(this as Control, Expand, true);
                }
                else
                {
                    translate.Y = -content.ActualHeight;
                    VisualStateManager.GoToState(this as Control, Collapse, true);
                }

 


For brevity’s sake, I dropped the code surrounding the if construct this time. This looks like it should do what we want it to and indeed it does. However, those of you with a sharp eye (or fast computers) may have noticed something. As soon as the panel first appears on screen, you see the little arrow in the title bar going from the expanded to the collapsed state. Which is to say that while the GoToState does set the visual state to Collapsed, it does so using the animation defined by that state. From Silverlight’s point of view of course this is the right thing to do. However, it doesn’t feel quite right from our perspective. So how do we fix this?

To fix the problem we must first understand how GoToState works. When GoToState is called with a control and a visual state, it simply digs out the corresponding state’s storyboard and begin’s playing it. It does not care whether or not the story board contains an animation. You might have noticed the third parameter to GoToState that I have not discussed so far. This third parameter tells GoToState whether it should get to the state it is going to using transitions along the way. You would think that setting the parameter to false like this:

VisualStateManager.GoToState(this as Control, Collapse, false);

would finally fix the issue for us. But that won't be the case, because as I said earlier, the GoToState method does not know whether or not the Expand state's storyboard is an animation. Fortunately, the people who wrote Visual State Manager thought of this scenario and this is where VisualTransition comes in. The documentation has this to say about VisualTransition:

(VisualTransition) Represents the visual behaviour that occurs when the control transitions from one state to another

That is to say that while the storyboards contained within visual states define how we want our control to look when it is in a particular state, visual transitions define how our control gets from one state to another. A visual transition, like visual states is defined in the control's template. Let's take a look at what the CollapsiblePanel's template would look like with a visual transition that takes it from the Expanded to the Collapsed state (and vice versa):

<vsm:Style TargetType="local:CollapsiblePanel">
        <vsm:Setter Property="Template">
            <vsm:Setter.Value>
                <ControlTemplate TargetType="local:CollapsiblePanel">
                    <Grid>                    
                        <vsm:VisualStateManager.VisualStateGroups>                           
                            <vsm:VisualStateGroup x:Name="CommonStates">
                                <vsm:VisualState x:Name="Collapse">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                            <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0">
                                                <SplineDoubleKeyFrame.KeySpline>
                                                    <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                </SplineDoubleKeyFrame.KeySpline>
                                            </SplineDoubleKeyFrame>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </vsm:VisualState>                               
                                <vsm:VisualState x:Name="Expand">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                            <SplineDoubleKeyFrame KeyTime="00:00:00" Value="90">
                                                <SplineDoubleKeyFrame.KeySpline>
                                                    <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                </SplineDoubleKeyFrame.KeySpline>
                                            </SplineDoubleKeyFrame>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </vsm:VisualState>
                               
                               
                                <vsm:VisualStateGroup.Transitions>
                                    <vsm:VisualTransition x:Name="Collapse2Expand" From="Collapse" To="Expand" Duration="00:00:01">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                <SplineDoubleKeyFrame KeyTime="00:00:01" Value="90">
                                                    <SplineDoubleKeyFrame.KeySpline>
                                                        <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                    </SplineDoubleKeyFrame.KeySpline>
                                                </SplineDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </vsm:VisualTransition>
                                    <vsm:VisualTransition x:Name="Expand2Collapse" From="Expand" To="Collapse" Duration="00:00:01">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0">
                                                    <SplineDoubleKeyFrame.KeySpline>
                                                        <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                    </SplineDoubleKeyFrame.KeySpline>
                                                </SplineDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </vsm:VisualTransition>
                                </vsm:VisualStateGroup.Transitions>

                            </vsm:VisualStateGroup>
                        </vsm:VisualStateManager.VisualStateGroups>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="20" />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <Grid x:Name="ExpandCollapseButton" Height="20" Grid.Row="0">
                            <Path x:Name="TitleBack" Opacity="0.8" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch" Stretch="Fill" StrokeThickness="0.5" Data="M12.5,7 C47.333332,7 115.85664,7 117,7 C118.14336,7 122.1255,6.7291665 122.25,12 C122.3745,17.270834 122.25,18.333334 122.25,21.5 L12.5,21.5 z">
                                <Path.Fill>
                                    <RadialGradientBrush GradientOrigin="0.699000000953674,0.792999982833862">
                                        <RadialGradientBrush.RelativeTransform>
                                            <TransformGroup>
                                                <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1.4" ScaleY="2.188"/>
                                                <SkewTransform CenterX="0.5" CenterY="0.5"/>
                                                <RotateTransform CenterX="0.5" CenterY="0.5"/>
                                                <TranslateTransform X="0.017" Y="0.009"/>
                                            </TransformGroup>
                                        </RadialGradientBrush.RelativeTransform>
                                        <GradientStop Color="#FF00008B" Offset="1"/>
                                        <GradientStop Color="#FFADD8E6" Offset="0"/>
                                    </RadialGradientBrush>
                                </Path.Fill>
                            </Path>
                            <TextBlock Cursor="Arrow" HorizontalAlignment="Stretch" Margin="27.75,2.75,-5,1.75" VerticalAlignment="Stretch" FontFamily="Verdana" FontSize="11" FontStyle='Normal' FontWeight='Normal' Foreground='#FFFFFFFF' Text='{TemplateBinding Title}' Opacity='1' x:Name='Title'/>
                      <Path x:Name="Arrow" HorizontalAlignment="Left" Margin="5.85300016403198,2.81200003623962,0,3.29800009727478" VerticalAlignment="Stretch" Width="10.685" Fill="#FFFFFFFF" Stretch="Fill" Stroke="#FF000000" StrokeThickness="0" Data="M182.75038,211.50015 L216.5,234.50017 L182.81238,257.87216 z" RenderTransformOrigin="0.5,0.5">
                       <Path.RenderTransform>
                        <TransformGroup>
                         <RotateTransform x:Name="ArrowAngleTransform" Angle="90"/>
                        </TransformGroup>
                       </Path.RenderTransform>
                      </Path>  
                  </Grid>
                    <Grid x:Name="ContentContainer" Grid.Row="1">
                        <Border x:Name="PanelContent" BorderThickness="0">
                            <Grid>
                                    <Rectangle Opacity="0.6" Stroke="Gray" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
                                    <Rectangle Opacity="0.6" Stroke="Black" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                                        <Rectangle.RenderTransform>
                                            <TranslateTransform X="-1" Y="-1" />
                                        </Rectangle.RenderTransform>
                                    </Rectangle>
                                    <ContentPresenter />
                            </Grid>
                        </Border>
                    </Grid>
                    </Grid>
                </ControlTemplate>
            </vsm:Setter.Value>
        </vsm:Setter>
    </vsm:Style>

I highlighted the new code in red. Also notice that I changed the duration on the storyboards for the states. Both the Collapse and Expand storyboard now run for 0 seconds, which is to say that they are instantaneous. As you can see, the transitions live in a collection hanging off of the Visual State Group object. Each transition expects to be provided the name of a start state, and end state and the duration for which the transition must run. The storyboard for the transition looks exactly like the state storyboard (it doesn't have to though) except that it lasts for a second. Now that we have separated the states from their transitions this code:

VisualStateManager.GoToState(this as Control, Collapse, false);

should do what we want. Setting the last parameter to false means that while it will run the state storyboard, it will ignore the transition storyboard. When the parameter is true the transition storyboad is run first and upon its completion the state storyboard is begun. Let's take a look at what the _content_SizeChanged method finally ends up looking like:

void _content_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            FrameworkElement content = sender as FrameworkElement;
            if (content != null)
            {
                TransformGroup tGroup = new TransformGroup();
                TranslateTransform translate = new TranslateTransform();
                translate.SetValue(FrameworkElement.NameProperty, "RollTransform" + Guid.NewGuid().ToString());
                if (IsExpanded)
                {
                    translate.Y = 0;
                    VisualStateManager.GoToState(this as Control, Expand, false);
                }
                else
                {
                    translate.Y = -content.ActualHeight;
                    VisualStateManager.GoToState(this as Control, Collapse, false);
                }

                tGroup.Children.Add(translate);
                content.RenderTransform = tGroup;

                _RollUpStoryboardName = "RollUp" + Guid.NewGuid().ToString();
                _RollDownStoryboardName = "RollDown" + Guid.NewGuid().ToString();

                _SetupYTranslationStoryboard(translate, _RollUpStoryboardName, -content.ActualHeight);
                _SetupYTranslationStoryboard(translate, _RollDownStoryboardName, 0);
            }
        }

There you go! Now our brand new content control supports visual states as well as visual transitions. That's all the features of the Visual State Manager covered! We have ensured that CollapsiblePanel respects the initial value of IsExpanded and also fixed that annoying little bug :)

For my next post, I will take a look at how to change our panel so that we can retemplate it to collapse horizontally. If you've been reading this series and enjoying the posts on this blog, please do leave behind a comment and rate this post. The latest code for CollapsiblePanel (along with the changes for this post) is available here:

The previous post in this series is available here.

Leave a Comment
  • Please add 5 and 3 and type the answer here:
  • Post
  • PingBack from http://blogs.msdn.com/knowledgecast/archive/2008/07/14/Writing-a-Silverlight-Content-Control.aspx

  • Hi,

    Great post, thank you :) It was very helpful, and still... so I decided to apply the collapsible content control idea to my collapsible menu control.

    Daniel

  • @Daniel: You're very welcome. It would be great if you could send me a URL to what you built. I'd be very interested in seeing it.

  • thanks alot for the post. it is a very cool little tool.

    are there any plans to extend it so that expandible controls are within expandible controls eg resize on the fly when disabled??

    thanks again Arunjeet

    ben.

  • @ben You're very welcome :) Not sure I quite understand what you mean. If you give me a little more detail one "resize on the fly when disabled" I might be able to accomplish what you want.

  • If you need to define two or more collapsible panels in grid or panel then this code doesnt work. For example:

    <xc:CollapsiblePanel Grid.Column="0" Grid.Row="0" Margin="2,2,2,2" Title="Continent" FontSize="10" Width="200">

    <StackPanel>

    <CheckBox Width="90" Content="Europe"/>

    <CheckBox Width="90" Content="America"/>

    <CheckBox Width="90" Content="Asia"/>

    </StackPanel>

    </xc:CollapsiblePanel>

    <xc:CollapsiblePanel Grid.Column="0" Grid.Row="1" Margin="2,2,2,2" Title="Product" FontSize="10" Width="200">

    <StackPanel>

    <CheckBox Width="90" Content="Software"/>

    <CheckBox Width="90" Content="Hardware"/>

    </StackPanel>

    </xc:CollapsiblePanel>

  • @Alex I threw that code into a XAML and it worked as expected. Here's the XAML I used:

    <UserControl x:Class="Alex.Page"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

                xmlns:xc="clr-namespace:Knowledgecast.Controls;assembly=Knowledgecast.Controls"

       Width="400" Height="300">

       <Grid x:Name="LayoutRoot" Background="White">

           <Grid.RowDefinitions>

               <RowDefinition />

               <RowDefinition />

           </Grid.RowDefinitions>

           <xc:CollapsiblePanel Grid.Column="0" Grid.Row="0" Margin="2,2,2,2" Title="Continent" FontSize="10" Width="200">

               <StackPanel>

                   <CheckBox Width="90" Content="Europe"/>

                   <CheckBox Width="90" Content="America"/>

                   <CheckBox Width="90" Content="Asia"/>

               </StackPanel>

           </xc:CollapsiblePanel>

           <xc:CollapsiblePanel Grid.Column="0" Grid.Row="1" Margin="2,2,2,2" Title="Product" FontSize="10" Width="200">

               <StackPanel>

                   <CheckBox Width="90" Content="Software"/>

                   <CheckBox Width="90" Content="Hardware"/>

               </StackPanel>

           </xc:CollapsiblePanel>

       </Grid>

    </UserControl>

    Perhaps if you could provide some more details on the problem you're facing. Does the code throw an exception?

  • I noticed how; even when you derive from ContentControl, you do not use any of the properties inherited from this class, like Content, and ContentTemplate or the protected method OnContentChanged.

    Is there any reason why you chose not to use them? A bug that prevents you from doing so, maybe?

    I was wondering because I am about to start creating a control and would like to know whether it is common/recommended practice to ommit these in the implementation.

    Thanks!

  • This was something that had been bothering me for a while. Silverlight 2 came out almost two months ago

  • Phew! It's been a long time since my last post. I have no excuse. I've been a lazy bum and I know it.

  • Why is there such a large gap between Collapsible Panels when collapsed? Shouldn't the top of the next panel touch the base of the panel above? Instead it appears that the panel fails to release the space it's sub-panels occupied.

  • Hello, The collapsible panel is an excellent control you have created.

    Can you tell me if it is possible to get the reference to the collapsible panel from the control contained inside it? And vice versa, How to get the reference to the control contained inside the collapsible panel from the collapsible panel?

  • Hi Arunjeet, Did anyone try to embed your collapsiblePanel  control as content inside youre collapsiblePanel. When the content of the collapsiblePanel changes in size it will not update its parents and/or children resulting in area's not visible or other visual flaws. I looked everywhere, but I cannot seem to find a way to access the content inside the contentpresenter to be able to update the parent(s) about their content size changes. I hope you could help me on this one. Thanks in advance. With kind regards, Magiel

Page 1 of 1 (13 items)