I am attempting to create my own template for an Expander
control. When the control is expanded, I want the content to slide down slowly.
The desired height of the content is not known at compile time.
I thought we could define the slide down as an animation:
<Storyboard x:Key="ExpandContent">
<DoubleAnimation
Storyboard.TargetName="_expanderContent"
Storyboard.TargetProperty="Height"
From="0.0"
To="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
Duration="0:0:1.0" />
</Storyboard>
But Unfortunately not. We get an error
Cannot freeze this Storyboard timeline tree for use across threads.
It appears that we cannot use binding when defining animation parameters. (Discussed also in this question.)
Does anyone have any ideas on how I can approach this? I'm wary of using LayoutTransform.ScaleY
, because that would create a distorted image.
This is similar to this question, but this question has an answer involved writing code-behind, which I don't think is possible in a control template. I'm wondering if a XAML-based solution is achievable.
<ControlTemplate x:Key="ExpanderControlTemplate" TargetType="{x:Type Expander}">
<ControlTemplate.Resources>
<!-- Here are the storyboards which don't work -->
<Storyboard x:Key="ExpandContent">
<DoubleAnimation
Storyboard.TargetName="_expanderContent"
Storyboard.TargetProperty="Height"
From="0.0"
To="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
Duration="0:0:1.0" />
</Storyboard>
<Storyboard x:Key="ContractContent">
<DoubleAnimation
Storyboard.TargetName="_expanderContent"
Storyboard.TargetProperty="Height"
From="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
To="0.0"
Duration="0:0:1.0" />
</Storyboard>
</ControlTemplate.Resources>
<Grid Name="MainGrid" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Name="ContentRow" Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter ContentSource="Header" />
<ToggleButton Template="{StaticResource ProductButtonExpand}"
Grid.Column="1"
IsChecked="{Binding Path=IsExpanded,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
/>
<Rectangle Grid.ColumnSpan="2" Fill="#FFDADADA" Height="1" Margin="8,0,8,2" VerticalAlignment="Bottom"/>
</Grid>
</Border>
<ContentPresenter Grid.Row="1" HorizontalAlignment="Stretch" Name="_expanderContent">
</ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="_expanderContent" Property="Height" Value="{Binding ElementName=_expanderContent,Path=DesiredHeight}" />
<!-- Here is where I would activate the storyboard if they did work -->
<Trigger.EnterActions>
<!--<BeginStoryboard Storyboard="{StaticResource ExpandContent}"/>-->
</Trigger.EnterActions>
<Trigger.ExitActions>
<!--<BeginStoryboard x:Name="ContractContent_BeginStoryboard" Storyboard="{StaticResource ContractContent}"/>-->
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="_expanderContent" Property="Height" Value="0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
There is a ready-to-use and XAML-only solution on CodeProject:
The Styles:
<local:MultiplyConverter x:Key="MultiplyConverter" />
<Style TargetType="Expander" x:Key="VerticalSlidingEmptyExpander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<ScrollViewer x:Name="ExpanderContentScrollView"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Top"
>
<ScrollViewer.Tag>
<system:Double>0.0</system:Double>
</ScrollViewer.Tag>
<ScrollViewer.Height>
<MultiBinding Converter="{StaticResource MultiplyConverter}">
<Binding Path="ActualHeight" ElementName="ExpanderContent"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ScrollViewer.Height>
<ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
</ScrollViewer>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="1"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="0"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Expander" x:Key="HorizontalSlidingEmptyExpander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<ScrollViewer x:Name="ExpanderContentScrollView"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Stretch"
>
<ScrollViewer.Tag>
<system:Double>0.0</system:Double>
</ScrollViewer.Tag>
<ScrollViewer.Width>
<MultiBinding Converter="{StaticResource MultiplyConverter}">
<Binding Path="ActualWidth" ElementName="ExpanderContent"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ScrollViewer.Width>
<ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
</ScrollViewer>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="1"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="0"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MultiplyConverter:
public class MultiplyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
double result = 1.0;
for (int i = 0; i < values.Length; i++)
{
if (values[i] is double)
result *= (double)values[i];
}
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new Exception("Not implemented");
}
}
I duplicated the Style to have a horizontal and vertical version and omitted the ToggleButtons, but you can easily get that from the original post.
This is kind of an old question but I had problems with this today, so I guess posting my solution would be worth it:
I had to animate the Height property of a grid row (sliding up and down), but needed dynamic binding so that the row would slide again to the same position as before.
I found this answer to be very helpful (after fruitlessly battling XAML): http://go4answers.webhost4life.com/Question/found-solution-work-protected-override-190845.aspx
Sometimes doing things in the code-behind is just simpler:
Storyboard sb = new Storyboard();
var animation = new GridLengthAnimation
{
Duration = new Duration(500.Milliseconds()),
From = this.myGridRow.Height,
To = new GridLength(IsGridRowVisible ? GridRowPreviousHeight : 0, GridUnitType.Pixel)
};
// Set the target of the animation
Storyboard.SetTarget(animation, this.myGridRow);
Storyboard.SetTargetProperty(animation, new PropertyPath("Height"));
// Kick the animation off
sb.Children.Add(animation);
sb.Begin();
The GridLengthAnimation class can be found here: http://social.msdn.microsoft.com/forums/en-US/wpf/thread/da47a4b8-4d39-4d6e-a570-7dbe51a842e4/
If you can use Interactions
with FluidLayout
(Blend 4 SDK) you are in luck, it's really useful for those fancy animation things.
First set the content CP's Height to 0:
<ContentPresenter Grid.Row="1"
HorizontalAlignment="Stretch"
x:Name="_expanderContent"
Height="0"/>
To animate this, the Height
just needs to be animated to NaN
in the VisualState
that represents the expanded state (non-discrete animations would not let you use NaN
):
xmlns:is="http://schemas.microsoft.com/expression/2010/interactions"
<Grid x:Name="MainGrid" Background="White">
<VisualStateManager.CustomVisualStateManager>
<is:ExtendedVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ExpansionStates" is:ExtendedVisualStateManager.UseFluidLayout="True">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:1"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)"
Storyboard.TargetName="_expanderContent">
<DiscreteDoubleKeyFrame KeyTime="0" Value="NaN"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Collapsed"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- ... --->
That should be all that is necessary, the fluid layout will create the transition for you from there.
If you have a code-behind solution that would be fine, you can even use code-behind in dictionaries like this:
<!-- TestDictionary.xaml -->
<ResourceDictionary x:Class="Test.TestDictionary"
...>
//TestDictionary.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
namespace Test
{
partial class TestDictionary : ResourceDictionary
{
//Handlers and such here
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With