Suppose I have a ListBox
bound to an ObservableCollection
and I want to animate adding/removing of ListBoxItems
eg. FadeIn/Out, SlideDown/Up etc. How can I do that?
相关问题
- VNC control for WPF application
- WPF Binding from System.Windows.SystemParameters.P
- XAML: Applying styles to nested controls
- How can I add a horizontal line (“goal line”) for
- How to properly change a resource dictionary
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:
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
XAMLs
Huzzah!~
Heh. Since accepted solution is not work, let's try another round ;)
We can't use Unloaded event because ListBox (or other control) remove item from visual tree when it removed from original list. So main idea is to create shadow copy of provided ObservableCollection and bind list to it.
First of all - XAML:
Create ListBox, bind it to our shadow copy, set IsSynchronizedWithCurrentItem for correct support ICollectionView.CurrentItem (very useful interface), and set Loaded event on item view. This event handler need to associate view (which will be animated) and item (which will be removed).
Initialize everything:
And last, but not least, ShadowViewSource class (yeah, it's not perfect, but as proof-of-concept it works):
And final words. First of all it works. Next - this approach don't require any changes in existing code, workarounds via Deleting property, etc, etc, etc. Especially when implemented as single custom control. You have ObservableCollection, add items, remove, do whatever you want, UI will always try to correctly reflect this changes.
The accepted answer works for animating the addition of new items, but not for the removal of existing ones. This is because by the time the
Unloaded
event fires, the item has already been removed. The key to getting deletion to work is to add a "marked for deletion" concept. Being marked for deletion should trigger the animation, and the completion of the animation should trigger the actual deletion. There are probably a bunch of ways this idea could be implemented, but I got it to work by creating an attached behavior and by tweaking my viewmodels a bit. The behavior exposes three attached properties, all of which must be set on eachListViewItem
:Storyboard
. This is the actual animation you want to run when an item is removed.ICommand
. This is a command that will be executed when the animation is done running. It should execute code to actually remove the element from the databound collection.bool
. Set this to true when you decide to remove an item from the list (e.g. in a button click handler). As soon as the attached behavior sees this property change to true, it will begin the animation. And when the animation'sCompleted
event fires, it willExecute
thePerformRemoval
command.Here is a link to full source for the behavior and example usage (if it's bad form to direct to your own blog, I'll remove the link. I'd paste the code here, but it's fairly lengthy. I don't receive any money from the thing, if that makes a difference).
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 anEventTrigger
to control the storyboards.That's a right pain though. You're probably better creating a
DataTemplate
and handling theFrameworkElement.Loaded
andFrameworkElement.Unloaded
events in anEventTrigger
.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.
HTH, Stimul8d
Fade-out is likely to be impossible without re-writing the
ItemsControl
base implementation. The problem is that when theItemsControl
receives theINotifyCollectionChanged
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:
But the 'fade-out' equivalent never works as the container is already invisible and cannot be reset.
Even if you have your own custom container generator, you cannot overcome this issue
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.
Create two story boards for fade-in and fade-out and bind its value to the brush you've created for the
OpacityMask
of yourListBox