How can I implement fading in and out of Added/Removed ListItems
Solution 1:
After spending mad hours hunting the wilds of Google, I figure I should share how I solved this problem since it seems to be a pretty d*mn simple thing to need and yet WPF makes it ridiculously frustrating until you intimately understand how animation is implemented. Once you do, you realize FrameworkElement.Unloaded is a useless event for animation. I've seen many versions of this question all over StackOverflow (amongst others), with all sorts of hackish ways to solve this. Hopefully I can provide a most simple example that you can then fancy up for your many purposes.
I will not show Fade In example since that is covered by plenty of examples using the Loaded routed event already. It is Fading Out on item removal that is the royal pain in the *@$.
The major problem here stems from how Storyboards just get weird when you put them into Control/Data Templates / Styles. It is impossible to bind the DataContext (and thus your object's ID) to the Storyboard. The Completed event fires with zero idea of who it just finished on. Diving the visual tree is useless since all your data templated items have the same names for their containers! So sure, you could write up a function that goes and searches the entire collection for objects that have their removal flag property set, but that is ugly and honestly, just not something you ever want to admit writing on purpose. And it won't work if you have several objects being removed within the length of your animation of each other (which is my case). You could also just write a cleanup thread that does similar things and get lost in timing hell. No fun. I digress. On to the solution.
Assumptions:
- You are using ObservableCollection populated with some custom objects
- You use a DataTemplate to give these a custom look, hence why you want to animate their removal
- You bind the ObservableCollection to a ListBox (or something simple like it)
- You have INotifyPropertyChanged implemented on the class of objects in your OC.
Then the solution is quite simple really, painfully so if you spent any long amount of time trying to solve this.
Create a Storyboard that animates your fade out in the Window.Resources section of your window (above the DataTemplate).
(Optional) Define the Duration separately as a resource so you can avoid hard coding as much. Or just hard code the durations.
Make a public boolean property in your object class called "Removing", "isRemoving", whatev. Make sure you raise a Property Changed event for this field.
Create a DataTrigger that binds to your "Removing" property and on True plays the fade out storyboard.
Create a private DispatcherTimer object in your object class and implement a simple timer that has the same duration as your fade out animation and removes your object from the list in its tick handler.
Code example is below, which hopefully makes it all easy to grasp. I simplified the example as much as possible so you'll need to adapt it to your environment as it suits you.
Code Behinds
public partial class MainWindow : Window
{
public static ObservableCollection<Missiles> MissileRack = new ObservableCollection<Missiles>(); // because who doesn't love missiles?
public static Duration FadeDuration;
// main window constructor
public MainWindow()
{
InitializeComponent();
// somewhere here you'll want to tie the XAML Duration to your code-behind, or if you like ugly messes you can just skip this step and hard code away
FadeDuration = (Duration)this.Resources["cnvFadeDuration"];
//
// blah blah
//
}
public void somethread_ShootsMissiles()
{
// imagine this is running on your background worker threads (or something like it)
// however you want to flip the Removing flag on specific objects, once you do, it will fade out nicely
var missilesToShoot = MissileRack.Where(p => (complicated LINQ search routine).ToList();
foreach (var missile in missilesToShoot)
{
// fire!
missile.Removing = true;
}
}
}
public class Missiles
{
public Missiles()
{}
public bool Removing
{
get { return _removing; }
set
{
_removing = value;
OnPropertyChanged("Removing"); // assume you know how to implement this
// start timer to remove missile from the rack
start_removal_timer();
}
}
private bool _removing = false;
private DispatcherTimer remove_timer;
private void start_removal_timer()
{
remove_timer = new DispatcherTimer();
// because we set the Interval of the timer to the same length as the animation, we know the animation will finish running before remove is called. Perfect.
remove_timer.Interval = MainWindow.TrackFadeDuration.TimeSpan; // I'm sure you can find a better way to share if you don't like global statics, but I am lazy
remove_timer.Tick += new EventHandler(remove_timer_Elapsed);
remove_timer.Start();
}
// use of DispatcherTimer ensures this handler runs on the GUI thread for us
// this handler is now effectively the "Storyboard Completed" event
private void remove_timer_Elapsed(object sender, EventArgs e)
{
// this is the only operation that matters for this example, feel free to fancy this line up on your own
MainWindow.MissileRack.Remove(this); // normally this would cause your object to just *poof* before animation has played, but thanks to timer,
}
}
XAMLs
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test" Height="300" Width="300">
<Window.Resources>
<Duration x:Key="cnvFadeDuration">0:0:0.3</Duration> <!-- or hard code this if you really must -->
<Storyboard x:Key="cnvFadeOut" >
<DoubleAnimation Storyboard.TargetName="cnvMissile"
Storyboard.TargetProperty="Opacity"
From="1" To="0" Duration="{StaticResource cnvFadeDuration}"
/>
</Storyboard>
<DataTemplate x:Key="MissileTemplate">
<Canvas x:Name="cnvMissile">
<!-- bunch of pretty missile graphics go here -->
</Canvas>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Removing}" Value="true" >
<DataTrigger.EnterActions>
<!-- you could actually just plop the storyboard right here instead of calling it as a resource, whatever suits your needs really -->
<BeginStoryboard Storyboard="{StaticResource cnvFadeOut}" />
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox /> <!-- do your typical data binding and junk -->
</Grid>
</Window>
Huzzah!~
Solution 2:
Dr TJ's answer is right enough. Going down that route you'd have to wrap ObservableCollection<T>
and implement a BeforeDelete event,..then you could use an EventTrigger
to control the storyboards.
That's a right pain though. You're probably better creating a DataTemplate
and handling the FrameworkElement.Loaded
and FrameworkElement.Unloaded
events in an EventTrigger
.
I've put a quick sample together for you below. You'd have to sort out the remove code yourself but I'm sure you're up to it.
<ListBox>
<ListBox.ItemsSource>
<x:Array Type="sys:String">
<sys:String>One</sys:String>
<sys:String>Two</sys:String>
<sys:String>Three</sys:String>
<sys:String>Four</sys:String>
<sys:String>Five</sys:String>
</x:Array>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Opacity="0">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Duration="00:00:02"
From="0"
To="1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="FrameworkElement.Unloaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Duration="00:00:02"
From="1"
To="0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
HTH, Stimul8d
Solution 3:
Fade-out is likely to be impossible without re-writing the ItemsControl
base implementation. The problem is that when the ItemsControl
receives the INotifyCollectionChanged
event from the collection it immediately (and within deep private code) marks the item container as not visible (IsVisible
is a readonly property that gets its value from a hidden cache so cannot be accessed).
You can easily implement the fade-in in this way:
public class FadingListBox : ListBox
{
protected override void PrepareContainerForItemOverride(
DependencyObject element, object item)
{
var lb = (ListBoxItem)element;
DoubleAnimation anm = new DoubleAnimation(0, 1,
TimeSpan.FromMilliseconds(500));
lb.BeginAnimation(OpacityProperty, anm);
base.PrepareContainerForItemOverride(element, item);
}
}
But the 'fade-out' equivalent never works as the container is already invisible and cannot be reset.
public class FadingListBox : ListBox
{
protected override void ClearContainerForItemOverride(
DependencyObject element, object item)
{
var lb = (ListBoxItem) element;
lb.BringIntoView();
DoubleAnimation anm = new DoubleAnimation(
1, 0, TimeSpan.FromMilliseconds(500));
lb.BeginAnimation(OpacityProperty, anm);
base.ClearContainerForItemOverride(element, item);
}
}
Even if you have your own custom container generator, you cannot overcome this issue
protected override DependencyObject GetContainerForItemOverride()
{
return new FadingListBoxItem();
}
And this kind of makes sense, because if the container was still visible after the data it represents has disappeared, then you could theoretically click on the container (kicking off triggers, events etc) and experience some subtle bugs perhaps.