While working on a custom control for Windows Presentation Foundation I came across a scenario where I needed the control (which was derived from Window) to fade in when opened, and fade out when a close-button is clicked.
I'd written a control that did this in Windows Forms a while back (I was now porting parts of it to WPF), using timers and stuff like this. But since WPF contains such a lot of cool stuff for animations and storyboards I figured I should be able to do this declaratively in XAML.
In the template for the control, in the Triggers-section, I added the included XAML below. I hooked up the fade in animation to the Loaded-event on the window, and the fade out to the Click-event on the button the user presses to close the window.
1: <ControlTemplate.Triggers>
2: <EventTrigger RoutedEvent="Window.Loaded">
3: <EventTrigger.Actions>
4: <BeginStoryboard>
5: <Storyboard TargetProperty="Opacity">
6: <DoubleAnimation From="0" To="0.8" Duration="0:0:2"/>
7: </Storyboard>
8: </BeginStoryboard>
9: </EventTrigger.Actions>
10: </EventTrigger>
11: <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="PART_CloseButton">
12: <EventTrigger.Actions>
13: <BeginStoryboard>
14: <Storyboard x:Name="PART_CloseAnimation" TargetProperty="Opacity">
15: <DoubleAnimation From="0.8" To="0" Duration="0:0:2"/>
16: </Storyboard>
17: </BeginStoryboard>
18: </EventTrigger.Actions>
19: </EventTrigger>
20: </ControlTemplate.Triggers>
This worked fine. The window faded in when opened, and faded out when closed. But since all the fade out actually did was hide the window by making it opaque I wanted to come up with a way to actually get rid of the window (actually call Close on it).
The Storyboard has a "Completed"-event, and that seemed like the logical place to call an event handler. In the OnApplyTemplate-method of my window I was already doing some stuff where I was hooking up events to elements in the template, so that seemed like a logical place to hook up events to the storyboard as well.
It turns out that i couldn't get access to "PART_CloseAnimation" and hook up the event handler. I tried Template.FindName, I tried looking in Template.Resources, and I did the same on my window. I couldn't get to the storyboard. The reason turned out to be quite simple. When a ContentTemplate is loaded it becomes sealed, and can not be changed. Therefore you can't change it afterwards, and do things like hook up event handlers to it.
I couldn't come up with a way to get around this declaratively, so I ended up doing the fade in and fade out procedurally instead. It's dead easy to create the animation in code, for example like this:
1: _fadeInAnimation = new DoubleAnimation();
2: _fadeInAnimation.From = 0; 3: _fadeInAnimation.To = 0.8;4: _fadeInAnimation.Duration = new Duration(TimeSpan.Parse("0:0:2"));
5: BeginAnimation(MyWindow.OpacityProperty, _fadeInAnimation);Of course, this is not as nice as doing it declaratively in the template, as this aspect of the behavior of my control can no longer be changed through skins and templates. But I guess I have to live with that.
By the way, the stuff above was done in Visual Studio 2008 and on version 3.5 of the .NET Framework. Just in case you try it on 3.0 and it happens to work differently.
Nicke, I've found a way to do this. It's a bit of a chore, but I think allowing people to change the open/close animations without subclassing is most important.
Posted by: Charlie | Tuesday, March 25, 2008 at 23:26
I haven't had time to work on this for a while, so I haven't explored the issue further. If you've found a solution to this, could you share it?
Posted by: Nicke | Wednesday, March 26, 2008 at 11:57
If you put the fading into the control, not in its template you will be able to access the methods of the class representing the control. So you will be able to handle the Completed event. You may also read the corresponding article in the documentation to be able to ensure that the Completed event is raised when you want it.
Posted by: Andreas | Thursday, March 27, 2008 at 10:45
The problem here was that I wanted the control tom be templateable.
Posted by: Nicklas Andersson | Thursday, March 27, 2008 at 11:17
Sorry, I did not notice that the whole animation had to be part of the template in your case. Of course it does only make sense if the whole control is animated.
VS 2008 creates the event handlers, even if the were not accessible. Perhaps any settings regarding filling (FillBehavior) may resolve your problem - I really don't know, sorry.
Posted by: Andreas | Thursday, March 27, 2008 at 13:30
I recently ran into this same problem - animating a window close, or more specifically, executing a command from a storyboard. I achieved this using the attached property (the cure-all for bridging xaml and object models without code-behind, so I'm told). At the end of the close storyboard, I added the following timeline (note that xml markup was 'marked up' for sake of posting):
[ObjectAnimationUsingKeyFrames BeginTime="00:00:03" Storyboard.TargetName="closeButton" Storyboard.TargetProperty="(behaviors:StoryboardCompletedCommandExecutor.ExecuteCommand)" Duration="00:00:00.0010000"]
[DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static ApplicationCommands.Close}"/]
[/ObjectAnimationUsingKeyFrames]
The button which triggers the animation contains standard xaml with the optional CommandParameter attribute normally set to {Binding}. Note that the Command attribute is removed otherwise you get the standard behavior of the window immediately closing with no animation.
And for the attached property (which as you may have noticed above we call behaviors), the PropertyChangedCallback is coded as:
private static void ExecuteCommandChanged( DependencyObject depObj, DependencyPropertyChangedEventArgs args )
{
RoutedUICommand command = ApplicationCommands.NotACommand;
if ( args.NewValue is RoutedUICommand )
{
command = (RoutedUICommand)args.NewValue;
}
if ( ( command != null ) && ( command != ApplicationCommands.NotACommand ) )
{
FrameworkElement target = depObj as FrameworkElement;
if ( target != null )
{
object parameter = null;
ButtonBase button = depObj as ButtonBase;
if ( button != null )
{
parameter = button.CommandParameter;
}
if ( command.CanExecute( parameter, target ) )
{
command.Execute( parameter, target );
}
}
}
}
The code was written specifically for our need at the time, so perhaps it could be genericized. Our close is triggered by a close button, so special casing was added to pull the command parameter from the source button if it is available.
Posted by: Parx | Thursday, August 21, 2008 at 22:46