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.
For what it's worth, here is the current state of my control template.
<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>
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/
/// <summary>
/// Animates a grid length value just like the DoubleAnimation animates a double value
/// </summary>
public class GridLengthAnimation : AnimationTimeline
{
/// <summary>
/// Returns the type of object to animate
/// </summary>
public override Type TargetPropertyType
{
get
{
return typeof(GridLength);
}
}
/// <summary>
/// Creates an instance of the animation object
/// </summary>
/// <returns>Returns the instance of the GridLengthAnimation</returns>
protected override System.Windows.Freezable CreateInstanceCore()
{
return new GridLengthAnimation();
}
/// <summary>
/// Dependency property for the From property
/// </summary>
public static readonly DependencyProperty FromProperty = DependencyProperty.Register("From", typeof(GridLength),
typeof(GridLengthAnimation));
/// <summary>
/// CLR Wrapper for the From depenendency property
/// </summary>
public GridLength From
{
get
{
return (GridLength)GetValue(GridLengthAnimation.FromProperty);
}
set
{
SetValue(GridLengthAnimation.FromProperty, value);
}
}
/// <summary>
/// Dependency property for the To property
/// </summary>
public static readonly DependencyProperty ToProperty = DependencyProperty.Register("To", typeof(GridLength),
typeof(GridLengthAnimation));
/// <summary>
/// CLR Wrapper for the To property
/// </summary>
public GridLength To
{
get
{
return (GridLength)GetValue(GridLengthAnimation.ToProperty);
}
set
{
SetValue(GridLengthAnimation.ToProperty, value);
}
}
/// <summary>
/// Animates the grid let set
/// </summary>
/// <param name="defaultOriginValue">The original value to animate</param>
/// <param name="defaultDestinationValue">The final value</param>
/// <param name="animationClock">The animation clock (timer)</param>
/// <returns>Returns the new grid length to set</returns>
public override object GetCurrentValue(object defaultOriginValue,
object defaultDestinationValue, AnimationClock animationClock)
{
double fromVal = ((GridLength)GetValue(GridLengthAnimation.FromProperty)).Value;
//check that from was set from the caller
if (fromVal == 1)
//set the from as the actual value
fromVal = ((GridLength)defaultDestinationValue).Value;
double toVal = ((GridLength)GetValue(GridLengthAnimation.ToProperty)).Value;
if (fromVal > toVal)
return new GridLength((1 - animationClock.CurrentProgress.Value) * (fromVal - toVal) + toVal, GridUnitType.Star);
else
return new GridLength(animationClock.CurrentProgress.Value * (toVal - fromVal) + fromVal, GridUnitType.Star);
}
}
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
}
}
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.
Related
I am using a Togglebutton Style from here after Binding the IsChecked Property to my ViewModel the Program crashes with a System.InvalidOperationException Ellipse not found in "System.Windows.Controls.ControlTemplate".
The Style looks like this:
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Viewbox>
<Border x:Name="Border"
CornerRadius="10"
Background="#FFE2E2E2"
Width="40"
Height="20">
<Border.Effect>
<DropShadowEffect ShadowDepth="0.5"
Direction="0"
Opacity="0.3" />
</Border.Effect>
<Ellipse x:Name="Ellipse"
Fill="#FF909090"
Stretch="Uniform"
Margin="-8 -4"
Stroke="Gray"
StrokeThickness="0.2"
HorizontalAlignment="Stretch">
<Ellipse.Effect>
<DropShadowEffect BlurRadius="10"
ShadowDepth="1"
Opacity="0.3"
Direction="260" />
</Ellipse.Effect>
</Ellipse>
</Border>
</Viewbox>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Checked">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Ellipse"
Storyboard.TargetProperty="(Ellipse.Fill).(SolidColorBrush.Color)"
To="#00facc"
Duration="0:0:0.05"
AccelerationRatio="0.7"
DecelerationRatio="0.3" />
<ThicknessAnimation Storyboard.TargetName="Ellipse"
Storyboard.TargetProperty="Margin"
To="20 -4 -8 -4"
Duration="0:0:0.15"
AccelerationRatio="0.7"
DecelerationRatio="0.3" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Unchecked">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Ellipse"
Storyboard.TargetProperty="(Ellipse.Fill).(SolidColorBrush.Color)"
To="#00facc"
Duration="0:0:0.05"
AccelerationRatio="0.7"
DecelerationRatio="0.3" />
<ThicknessAnimation
Storyboard.TargetName="Ellipse"
Storyboard.TargetProperty="Margin"
To="-8 -4"
Duration="0:0:0.15"
AccelerationRatio="0.7"
DecelerationRatio="0.3" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And my usage like this:
<ToggleButton Grid.Column="0"
Grid.Row="2"
IsChecked="{Binding IsAutostop}"/>
I'd like to use the style AND be able to use the "IsChecked" Property.
EDIT
Thanks for the comments.
I open the window with a Service bound to a command in a taskbaricon.
The Service:
class WindowService
{
public void OpenOptionsView(MainViewModel mvm)
{
OptionsView optionsView = new OptionsView { DataContext = mvm };
optionsView.Show();
}
}
The important parts of the MainviewModel:
public ICommand OpenOptionsViewCommand { get; private set; }
public bool IsAutostop { get { return Settings.Default.Autostop; } set { Settings.Default.Autostop = value; Settings.Default.Save(); OnPropertyChanged(); } }
public MainViewModel()
{
OpenOptionsViewCommand = new RelayCommand(OpenOptionsView, param => true);
}
private void OpenOptionsView(object o)
{
WindowService ws = new WindowService();
ws.OpenOptionsView(this);
}
I stored my TaskbarIcon in a seperated View:
<tb:TaskbarIcon>
<tb:TaskbarIcon.ContextMenu>
<ContextMenu Background="#1e1e1e">
<MenuItem Header="Programm öffnen"
Foreground="White"
Command="{Binding OpenOptionsViewCommand}"/>
</ContextMenu>
</tb:TaskbarIcon.ContextMenu>
</tb:TaskbarIcon>
EDIT II:
I just found out, that the problem occurs when the bound value is True and the window should be opened with a checked Togglebutton.
I am trying to change Background of a Border when user is dragging a file on it.
I want to define the effect using XAML only.
I tried the below but the Background is not changed when dragging a file on the Border.
<Border Name="dropBorder" BorderThickness="1" AllowDrop="True">
<Border.Triggers>
<EventTrigger RoutedEvent="DragOver">
<BeginStoryboard>
<Storyboard Storyboard.TargetProperty="Background">
<ColorAnimation From="Transparent" To="#FF444444" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock Text="Drag and drop file(s) here" Foreground="Gray" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10"/>
</Border>
I also tried to use DragEnter as below with no results
<EventTrigger RoutedEvent="Border.DragEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="dropBorder"
Storyboard.TargetProperty="Background"
Duration="0:0:0.5"
From="Transparent" To="#FF444444"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
I didnt quite meet your 100% requirement. I created an attached property, which I set via code-behind, so you will want to assess this. Also, moved the color animation around as you were trying to animate a brush, not a color.
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfApplication1="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<SolidColorBrush x:Key="SharedBackgroundBrush" Color="Transparent" />
</Window.Resources>
<Border Name="dropBorder" BorderThickness="1" AllowDrop="True" DragEnter="DropBorder_OnDragEnter" DragLeave="DropBorder_OnPreviewDragLeave" Background="{StaticResource SharedBackgroundBrush}">
<Border.Style>
<Style>
<Style.Triggers>
<Trigger Property="wpfApplication1:DragDropHelper.IsDragOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard Storyboard.Target="{StaticResource SharedBackgroundBrush}" Storyboard.TargetProperty="Color">
<ColorAnimation From="Transparent" To="Yellow" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard Storyboard.Target="{StaticResource SharedBackgroundBrush}" Storyboard.TargetProperty="Color">
<ColorAnimation From="Yellow" To="Transparent" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="Drag and drop file(s) here" Foreground="Gray" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10"/>
</Border>
</Window>
Code:
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void DropBorder_OnDragEnter(object sender, DragEventArgs e)
{
DragDropHelper.SetIsDragOver((DependencyObject)sender, true);
}
private void DropBorder_OnPreviewDragLeave(object sender, DragEventArgs e)
{
DragDropHelper.SetIsDragOver((DependencyObject)sender, false);
}
}
public class DragDropHelper
{
public static readonly DependencyProperty IsDragOverProperty = DependencyProperty.RegisterAttached(
"IsDragOver", typeof (bool), typeof (DragDropHelper), new PropertyMetadata(default(bool)));
public static void SetIsDragOver(DependencyObject element, bool value)
{
element.SetValue(IsDragOverProperty, value);
}
public static bool GetIsDragOver(DependencyObject element)
{
return (bool) element.GetValue(IsDragOverProperty);
}
}
}
I want a control (e.g. a GroupBox) to show a grow animation when it becomes visible and a shrink animation, when the visibility is changed to "Collapsed".
Therefore, I created a style which implements an animated grow and shrink effect as shown here in a small sample application (shown below).
However, only the grow animation is shown. Instead of showing the shrink animation, the groupbox disappears at once.
Can anyone tell me, why?
And even better, how to fix it?
<Window x:Class="ShrinkTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="FrameworkElement" x:Key="ExpandableElement">
<Setter Property="RenderTransformOrigin" Value="0.5 0" />
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform/>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" From="0" To="1" Duration="0:0:0.5" AccelerationRatio="0.2" DecelerationRatio="0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="Visibility" Value="Hidden">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" From="1" To="0" Duration="0:0:0.5" AccelerationRatio="0.2" DecelerationRatio="0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Margin="8" Width="140" Click="ButtonBase_OnClick">Expand / Shrink</Button>
<TextBlock Grid.Row="1" Text="--- Header ---"/>
<GroupBox x:Name="GroupBox" Grid.Row="2" Header="GroupBox" Style="{StaticResource ExpandableElement}" >
<StackPanel Orientation="Vertical">
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
<TextBlock Text="Test Test Test"/>
</StackPanel>
</GroupBox>
<TextBlock Grid.Row="3" Text="--- Footer ---"/>
</Grid>
</Window>
I had a similar problem. Just think about it for a minute... your problem is that you can see your animation when it's visible, but you can't when it is hidden. That is also your answer to why... because it is hidden. I know, that's fairly unsatisfactory answer, but that's just how it is.
As to how to fix it... well saying it is simple, but implementing it is not. Simply put, you have to run your animation until it ends and then set the Visibility to Hidden. So unfortunately this means that nice, simple setting the Visibility property in the Trigger is no longer viable... it's ok to make it visible, just not for hiding.
In my case, I have a whole framework that I built my animations into. Basically speaking though, when I remove items from the collections, internally the item is not actually removed, but instead its exit animation is started. Only when that animation is complete will the internal collection actually remove the item.
So if you can be bothered, then you'll have to implement something like this where, rather than setting the Visibility property to Hidden, you set another property to true which triggers the animation and when the Completed event from that animation is called, then you set the Visibility property to Hidden.
Sheridan is right. As soon as a control becomes invisible, it doesn't matter, which animation you apply to it. :-)
So I created a special ExpandingContentControl:
public class ExpandingContentControl : ContentControl
{
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
"IsExpanded", typeof(bool), typeof(ExpandingContentControl), new PropertyMetadata(false));
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
public ExpandingContentControl()
{
Visibility = IsExpanded ? Visibility.Visible : Visibility.Collapsed;
}
}
But there was also a problem with the style: Creating two triggers which are bound to different values of the same property obviously doesn't work.
Instead, I'm now using just one trigger where the EnterAction implements growing and the ExitAction implements shrinking the control:
<Style TargetType="controls:ExpandingContentControl" >
<Setter Property="RenderTransformOrigin" Value="0.5 1" />
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform/>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" >
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" KeyTime="00:00:00"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" From="0" To="1"
Duration="0:0:0.3" DecelerationRatio="0.4"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleY" From="1" To="0" Duration="0:0:0.2" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" KeyTime="00:00:0.2"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
I encounter a problem that I cannot solve. I hope to find an answer here. I need a listbox to hide half way when a certain listboxitem is selected. I setup a storyboard with opacity mask animation which work fine in blend. My problem I cannot initiate BeginStoryboard. I tried numerous ways and no success. I need to hide the listbox to reveal the content behind it. I generate listboxitems from XML data file and based on the name node I planned to initiate storyboard playing.
Here what I have.
I created DataTemplate which I set in ListBoxItem Style:
<DataTemplate x:Key="SelectedListBoxItemDataTemplate">
<StackPanel x:Name="DataItemSelected" Orientation="Horizontal" Margin="12,0,0,0" >
<TextBlock FontFamily="Arial" Text="►" VerticalAlignment="Center" HorizontalAlignment="Center" Visibility="{Binding XPath=state}" Margin="-4, 0,6,4"/>
<Image x:Name="ListBoxImage" Source="{Binding XPath=icon}" Margin="4,4,14,4" VerticalAlignment="Center" HorizontalAlignment="Center" Stretch="Uniform" />
<TextBlock x:Name="textBlock" Text="{Binding XPath=name}" LineHeight="22" Foreground="#FFFFFFFF" FontSize="16" />
<Border x:Name="PART_Icon" Background="{x:Null}" Width="{Binding NodeValue.Width}" HorizontalAlignment="Left" Padding="3,0"></Border>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding XPath=name}" Value="SERVERS">
<Setter TargetName="PART_Icon" Property="Background" Value="Black" />
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource HideListBox}" x:Name="HideListBox_BeginStoryboard"/>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
I need to run this storyboard which I keep in Window.Resources:
<Storyboard x:Key="HideListBox">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[0].(GradientStop.Offset)" Storyboard.TargetName="Nav_ListBox">
<EasingDoubleKeyFrame KeyTime="0" Value="0.069"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Offset)" Storyboard.TargetName="Nav_ListBox">
<EasingDoubleKeyFrame KeyTime="0" Value="0.069"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="Nav_ListBox">
<EasingColorKeyFrame KeyTime="0" Value="White"/>
<EasingColorKeyFrame KeyTime="0:0:0.4" Value="White"/>
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="Nav_ListBox">
<EasingColorKeyFrame KeyTime="0" Value="#00000000"/>
<EasingColorKeyFrame KeyTime="0:0:0.4" Value="#00000000"/>
</ColorAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(LinearGradientBrush.StartPoint)" Storyboard.TargetName="Nav_ListBox">
<EasingPointKeyFrame KeyTime="0" Value="1.076,0.501"/>
<EasingPointKeyFrame KeyTime="0:0:0.4" Value="1,0.5"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(LinearGradientBrush.EndPoint)" Storyboard.TargetName="Nav_ListBox">
<EasingPointKeyFrame KeyTime="0" Value="0.035,0.501"/>
<EasingPointKeyFrame KeyTime="0:0:0.4" Value="0.2,0.5"/>
</PointAnimationUsingKeyFrames>
</Storyboard>
I am getting errors that "Nav_ListBox" object cannot be found. I understand that listbox object is not avaible from the datatemplate level. I am wondering what will be the right solution to enable animation to play and eventualy to remove on click the othe listboxitem. Thank you in advance.
I put together something quick to hopefully help you on your way (new default WPF application, MainWindow's DataContext set to itself). I ended up using an IValueConverter to get the Name from the generated XmlLinkedNode out of the SelectedItem of the ListBox, but there should be a more elegant way using XPath statements I'm not familiar with. Basically declare your Storyboard on the ListBoxes Style, not in the Datatemplate:
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<XmlDataProvider x:Key="persons"
XPath="persons/person"
Source="xmldata.xml" />
<local:SelectionConverter x:Key="selectionConverter" />
</Window.Resources>
<Grid>
<ListBox Background="White" ItemsSource="{Binding Source={StaticResource persons}}" x:Name="lst">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding XPath=name}" />
<TextBlock Text="{Binding XPath=prop}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=lst, Path=SelectedItem, Converter={StaticResource selectionConverter}}"
Value="b">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard Duration="0:0:1">
<ColorAnimation Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)"
To="Green" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard Duration="0:0:1">
<ColorAnimation Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)"
To="White" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>
</Grid>
</Window>
MainWindow codebehind:
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
}
}
SelectionConverter.cs
namespace WpfApplication1
{
public class SelectionConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (value == null) ? null : (value as XmlLinkedNode).SelectNodes("name")[0].InnerText;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
Sample data (Add to your project as XML File):
<?xml version="1.0" encoding="utf-8" ?>
<persons>
<person>
<name>a</name>
<prop>3</prop>
</person>
<person>
<name>b</name>
<prop>3</prop>
</person>
<person>
<name>c</name>
<prop>3</prop>
</person>
</persons>
I have a data object -- a custom class called Notification -- that exposes a IsCritical property. The idea being that if a notification will expire, it has a period of validity and the user's attention should be drawn towards it.
Imagine a scenario with this test data:
_source = new[] {
new Notification { Text = "Just thought you should know" },
new Notification { Text = "Quick, run!", IsCritical = true },
};
The second item should appear in the ItemsControl with a pulsing background. Here's a simple data template excerpt that shows the means by which I was thinking of animating the background between grey and yellow.
<DataTemplate DataType="Notification">
<Border CornerRadius="5" Background="#DDD">
<Border.Triggers>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</DataTemplate>
What I'm unsure about is how to make this animation conditional upon the value of IsCritical. If the bound value is false, then the default background colour of #DDD should be maintained.
The final part of this puzzle is... DataTriggers. All you have to do is add one DataTrigger to your DataTemplate, bind it to IsCritical property, and whenever it's true, in it's EnterAction/ExitAction you start and stop highlighting storyboard. Here is completely working solution with some hard-coded shortcuts (you can definitely do better):
Xaml:
<Window x:Class="WpfTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Notification Sample" Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="NotificationTemplate">
<Border Name="brd" Background="Transparent">
<TextBlock Text="{Binding Text}"/>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCritical}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Name="highlight">
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"
Storyboard.TargetName="brd"
From="#DDD" To="#FF0" Duration="0:0:0.5"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="highlight"/>
</DataTrigger.ExitActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding Notifications}"
ItemTemplate="{StaticResource NotificationTemplate}"/>
<Button Grid.Row="1"
Click="ToggleImportance_Click"
Content="Toggle importance"/>
</Grid>
</Window>
Code behind:
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace WpfTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new NotificationViewModel();
}
private void ToggleImportance_Click(object sender, RoutedEventArgs e)
{
((NotificationViewModel)DataContext).ToggleImportance();
}
}
public class NotificationViewModel
{
public IList<Notification> Notifications
{
get;
private set;
}
public NotificationViewModel()
{
Notifications = new List<Notification>
{
new Notification
{
Text = "Just thought you should know"
},
new Notification
{
Text = "Quick, run!",
IsCritical = true
},
};
}
public void ToggleImportance()
{
if (Notifications[0].IsCritical)
{
Notifications[0].IsCritical = false;
Notifications[1].IsCritical = true;
}
else
{
Notifications[0].IsCritical = true;
Notifications[1].IsCritical = false;
}
}
}
public class Notification : INotifyPropertyChanged
{
private bool _isCritical;
public string Text { get; set; }
public bool IsCritical
{
get { return _isCritical; }
set
{
_isCritical = value;
InvokePropertyChanged("IsCritical");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void InvokePropertyChanged(string name)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
Hope this helps :).
What I would do is create two DataTemplates and use a DataTemplateSelector. Your XAML would be something like:
<ItemsControl
ItemsSource="{Binding ElementName=Window, Path=Messages}">
<ItemsControl.Resources>
<DataTemplate
x:Key="CriticalTemplate">
<Border
CornerRadius="5"
Background="#DDD">
<Border.Triggers>
<EventTrigger
RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD"
To="#FF0"
Duration="0:0:0.7"
AutoReverse="True"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock
Text="{Binding Path=Text}" />
</Border>
</DataTemplate>
<DataTemplate
x:Key="NonCriticalTemplate">
<Border
CornerRadius="5"
Background="#DDD">
<TextBlock
Text="{Binding Path=Text}" />
</Border>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplateSelector>
<this:CriticalItemSelector
Critical="{StaticResource CriticalTemplate}"
NonCritical="{StaticResource NonCriticalTemplate}" />
</ItemsControl.ItemTemplateSelector>
And the DataTemplateSelector would be something similar to:
class CriticalItemSelector : DataTemplateSelector
{
public DataTemplate Critical
{
get;
set;
}
public DataTemplate NonCritical
{
get;
set;
}
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
Message message = item as Message;
if(item != null)
{
if(message.IsCritical)
{
return Critical;
}
else
{
return NonCritical;
}
}
else
{
return null;
}
}
}
This way, WPF will automatically set anything that is critical to the template with the animation, and everything else will be the other template. This is also generic in that later on, you could use a different property to switch the templates and/or add more templates (A Low/Normal/High importance scheme).
It seems to be an odity with ColorAnimation, as it works fine with DoubleAnimation. You need to explicity specify the storyboards "TargetName" property to work with ColorAnimation
<Window.Resources>
<DataTemplate x:Key="NotificationTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsCritical}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
Storyboard.TargetName="border"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
<Border x:Name="border" CornerRadius="5" Background="#DDD" >
<TextBlock Text="{Binding Text}" />
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl x:Name="NotificationItems" ItemsSource="{Binding}" ItemTemplate="{StaticResource NotificationTemplate}" />
</Grid>
Here's a solution that only starts the animation when the incoming property update is a certain value. Useful if you want to draw the user's attention to something with the animation, but afterwards the UI should return to it's default state.
Assuming IsCritical is bound to a control (or even an invisible control) you add NotifyOnTargetUpdated to the binding and tie an EventTrigger to the Binding.TargetUpdated event. Then you extend the control to only fire the TargetUpdated event when the incoming value is the one you are interested in. So...
public class CustomTextBlock : TextBlock
{
public CustomTextBlock()
{
base.TargetUpdated += new EventHandler<DataTransferEventArgs>(CustomTextBlock_TargetUpdated);
}
private void CustomTextBlock_TargetUpdated(object sender, DataTransferEventArgs e)
{
// don't fire the TargetUpdated event if the incoming value is false
if (this.Text == "False") e.Handled = true;
}
}
and in the XAML file ..
<DataTemplate>
..
<Controls:CustomTextBlock x:Name="txtCustom" Text="{Binding Path=IsCritical, NotifyOnTargetUpdated=True}"/>
..
<DataTemplate.Triggers>
<EventTrigger SourceName="txtCustom" RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>..</Storyboard>
</BeginStoryboard>
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
You use style triggers in this case. (I'm doing this from memory so there might be some bugs)
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding IsCritical}" Value="true">
<Setter Property="Triggers">
<Setter.Value>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>