Comment puis-je implémenter la décoloration dans et hors de ListItems ajoutés/supprimés
Supposons que j'ai un ListBox
lié à un ObservableCollection
et je veux animer ajout/suppression de ListBoxItems
par exemple. FadeIn / Out, SlideDown / Up etc. Comment puis-je le faire?
7 réponses
La réponse du Dr TJ est juste. En descendant cette route, vous devrez envelopper ObservableCollection<T>
et implémenter un événement BeforeDelete,..ensuite, vous pouvez utiliser un EventTrigger
pour contrôler les story-boards.
C'est une douleur juste cependant. Vous feriez probablement mieux de créer un DataTemplate
et de gérer les événements FrameworkElement.Loaded
et FrameworkElement.Unloaded
dans un EventTrigger
.
J'ai mis un échantillon rapide ensemble pour vous ci-dessous. Vous devrez trier le code de suppression vous-même, mais je suis sûr que vous êtes à la hauteur.
<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
Après avoir passé des heures folles à chasser les terres sauvages de Google, je me dis que je devrais partager comment j'ai résolu ce problème car il semble être une jolie chose simple d*mn dont il faut avoir besoin et pourtant WPF le rend ridiculement frustrant jusqu'à ce que vous compreniez intimement comment l'animation est implémentée. Une fois que vous le faites, vous réalisez FrameworkElement.Unloaded est un événement inutile pour l'animation. J'ai vu beaucoup de versions de cette question partout dans StackOverflow (entre autres), avec toutes sortes de façons hackish de résoudre cela. Espérons-le Je peux fournir un exemple le plus simple que vous pouvez ensuite imaginer pour vos nombreuses fins.
Je ne montrerai pas de fondu dans l'exemple car cela est couvert par de nombreux exemples en utilisant déjà L'événement routé chargé. Il S'estompe sur la suppression de l'élément qui est la douleur royale dans le*@$.
Le problème majeur ici provient de la façon dont les Storyboards deviennent bizarres lorsque vous les mettez dans des modèles/styles de Contrôle / données. Il est impossible de lier le DataContext (et donc L'ID de votre objet) au Storyboard. L'événement terminé se déclenche avec aucune idée de qui il vient de finir. Plonger l'arbre visuel est inutile puisque tous vos éléments de données modélisés ont les mêmes noms pour leurs conteneurs! Donc, bien sûr, vous pouvez écrire une fonction qui va et recherche toute la collection pour les objets qui ont leur propriété de drapeau de suppression définie, mais c'est laid et honnêtement, tout simplement pas quelque chose que vous voulez jamais admettre écrire exprès. Et cela ne fonctionnera pas si vous avez plusieurs objets supprimés dans le longueur de votre animation les uns des autres (ce qui est mon cas). Vous pouvez également écrire un thread de nettoyage qui fait des choses similaires et se perdre dans l'enfer du timing. Pas de plaisir. Je m'éloigne du sujet. Sur la solution.
Hypothèses:
- vous utilisez ObservableCollection rempli avec des objets personnalisés
- vous utilisez un DataTemplate pour leur donner un aspect personnalisé, d'où la raison pour laquelle vous souhaitez animer leur suppression
- vous liez ObservableCollection à une ListBox (ou quelque chose de simple comme ça)
- Vous avez INotifyPropertyChanged implémenté sur la classe d'objets dans votre OC.
Alors la solution est assez simple vraiment, douloureusement donc si vous avez passé beaucoup de temps à essayer de résoudre ce problème.
Créez un Storyboard qui anime votre fondu dans la fenêtre.Section Ressources de votre fenêtre (au-dessus du DataTemplate).
(facultatif) définissez la durée séparément en tant que ressource afin d'éviter autant le codage en dur. Ou simplement coder les durées.
Créez une propriété booléenne publique dans votre classe d'objets appelée "Removing", "isRemoving", whatev. Assurez-vous de déclencher un événement de modification de propriété pour ce champ.
Créez un DataTrigger qui se lie à votre propriété "suppression" et sur True joue le storyboard fade out.
Créez un objet DispatcherTimer privé dans votre classe d'objets et implémentez une minuterie simple qui a la même durée que votre fondu animation et supprime votre objet de la liste dans son gestionnaire de tick.
L'exemple de Code est ci-dessous, ce qui, espérons-le, rend tout cela facile à saisir. J'ai simplifié l'exemple autant que possible, donc vous devrez l'adapter à votre environnement comme il vous convient.
Code Derrière
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!~
Fade-out est susceptible d'être impossible sans réécrire l'implémentation de la base ItemsControl
. Le problème est que lorsque ItemsControl
reçoit l'événement INotifyCollectionChanged
de la collection, il marque immédiatement (et dans le code privé profond) le conteneur d'élément comme non visible (IsVisible
est une propriété en lecture seule qui obtient sa valeur à partir d'un cache caché donc impossible d'accéder).
Vous pouvez facilement implémenter le fade-in de cette façon:
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);
}
}
Mais l'équivalent 'fade-out' ne fonctionne jamais car le conteneur est déjà invisible et ne peut pas être réinitialisé.
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);
}
}
Même si vous avez votre propre générateur de conteneur personnalisé, vous ne pouvez pas résoudre ce problème
protected override DependencyObject GetContainerForItemOverride()
{
return new FadingListBoxItem();
}
Et ce genre de sens, parce que si le conteneur était toujours visible après que les données qu'il représente a disparu, alors vous pouvez théoriquement cliquer sur le conteneur (lancer des déclencheurs, des événements, etc.) et éprouver quelques bugs subtils peut-être.
La réponse acceptée fonctionne pour animer l'ajout de nouveaux éléments, mais pas pour supprimer ceux qui existent déjà. En effet, au moment où l'événement Unloaded
se déclenche, l'élément a déjà été supprimé. La clé pour faire fonctionner la suppression est d'ajouter un concept "marqué pour la suppression". Étant marqué pour suppression devrait déclencher l'animation, et l'achèvement de l'animation devrait déclencher la suppression effective. Il y a probablement un tas de façons dont cette idée pourrait être mise en œuvre, mais je l'ai fait fonctionner en créant un comportement attaché et en peaufinant un peu mes viewmodels. Le comportement expose trois propriétés attachées, qui doivent toutes être définies sur chaque ListViewItem
:
- "Storyboard" de type
Storyboard
. C'est l'animation que vous souhaitez exécuter lorsqu'un élément est supprimé. - "PerformRemoval" de type
ICommand
. C'est une commande qui sera exécutée lorsque l'animation est terminée. Il devrait exécuter du code pour supprimer réellement l'élément de la base de données collection. - "IsMarkedForRemoval" de type
bool
. Activez cette option lorsque vous décidez de supprimer un élément de la liste (par exemple, en un clic de bouton gestionnaire). Dès que le comportement attaché voit cette propriété changer à true, il commencera l'animation. Et quand l'événementCompleted
de l'animation se déclenche, ilExecute
la commandePerformRemoval
.
Ici est un lien vers la source complète pour le comportement et l'exemple d'utilisation (si c'est une mauvaise forme à diriger vers votre propre blog, je vais supprimer le lien. Je collerais le code ici, mais c'est assez long. Je ne reçois pas d'argent de la chose, si cela fait une différence).
Créez deux story boards pour fade-in et fade-out et liez sa valeur au pinceau que vous avez créé pour le OpacityMask
de votre ListBox
Pour moi, l'événement FrameworkElement.Unloaded
ne fonctionne pas - l'élément disparaît instantanément. Je peux difficilement croire que des années d'expérience avec WPF n'ont rien produit de plus joli, mais on dirait que la seule façon dont cela peut fonctionner est un hack décrit ici: Animating removed item in Listbox ?..
Heh. Puisque la solution acceptée n'est pas un travail, essayons un autre tour ;)
Nous ne pouvons pas utiliser L'événement déchargé car ListBox (ou un autre contrôle) supprime l'élément de l'arborescence visuelle lorsqu'il est retiré de la liste d'origine. L'idée principale est donc de créer une copie fantôme de ObservableCollection fournie et de lier la liste à celle-ci.
Tout d'abord-XAML:
<ListBox ItemsSource="{Binding ShadowView}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Loaded="OnItemViewLoaded">
<TextBlock Text="{Binding}"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Créez une ListBox, liez-la à notre shadow copy, définissez IsSynchronizedWithCurrentItem pour un support correct ICollectionView.CurrentItem (très utile interface), et définir L'événement chargé sur la vue de l'élément. Ce gestionnaire d'événements doit associer la vue (qui sera animée) et l'élément (qui sera supprimé).
private void OnItemViewLoaded (object sender, RoutedEventArgs e)
{
var fe = (FrameworkElement) sender ;
var dc = (DependencyObject) fe.DataContext ;
dc.SetValue (ShadowViewSource.ViewProperty, fe) ;
}
Tout initialiser:
private readonly ShadowViewSource m_shadow ;
public ICollectionView ShadowView => m_shadow.View ;
public MainWindow ()
{
m_collection = new ObservableCollection<...> () ;
m_view = CollectionViewSource.GetDefaultView (m_collection) ;
m_shadow = new ShadowViewSource (m_view) ;
InitializeComponent ();
}
Et enfin, mais non des moindres, la classe ShadowViewSource (oui, ce n'est pas parfait, mais comme preuve de concept cela fonctionne):
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Collections.Specialized ;
using System.ComponentModel ;
using System.Linq ;
using System.Windows ;
using System.Windows.Data ;
using System.Windows.Media.Animation ;
namespace ShadowView
{
public class ShadowViewSource
{
public static readonly DependencyProperty ViewProperty = DependencyProperty.RegisterAttached ("View", typeof (FrameworkElement), typeof (ShadowViewSource)) ;
private readonly ICollectionView m_sourceView ;
private readonly IEnumerable<object> m_source ;
private readonly ICollectionView m_view ;
private readonly ObservableCollection<object> m_collection ;
public ShadowViewSource (ICollectionView view)
{
var sourceChanged = view.SourceCollection as INotifyCollectionChanged ;
if (sourceChanged == null)
throw new ArgumentNullException (nameof (sourceChanged)) ;
var sortChanged = view.SortDescriptions as INotifyCollectionChanged ;
if (sortChanged == null)
throw new ArgumentNullException (nameof (sortChanged)) ;
m_source = view.SourceCollection as IEnumerable<object> ;
if (m_source == null)
throw new ArgumentNullException (nameof (m_source)) ;
m_sourceView = view ;
m_collection = new ObservableCollection<object> (m_source) ;
m_view = CollectionViewSource.GetDefaultView (m_collection) ;
m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;
m_sourceView.CurrentChanged += OnSourceCurrentChanged ;
m_view.CurrentChanged += OnViewCurrentChanged ;
sourceChanged.CollectionChanged += OnSourceCollectionChanged ;
sortChanged.CollectionChanged += OnSortChanged ;
}
private void OnSortChanged (object sender, NotifyCollectionChangedEventArgs e)
{
using (m_view.DeferRefresh ())
{
var sd = m_view.SortDescriptions ;
sd.Clear () ;
foreach (var desc in m_sourceView.SortDescriptions)
sd.Add (desc) ;
}
}
private void OnSourceCollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
{
var toAdd = m_source.Except (m_collection) ;
var toRemove = m_collection.Except (m_source) ;
foreach (var obj in toAdd)
m_collection.Add (obj) ;
foreach (DependencyObject obj in toRemove)
{
var view = (FrameworkElement) obj.GetValue (ViewProperty) ;
var begintime = 1 ;
var sb = new Storyboard { BeginTime = TimeSpan.FromSeconds (begintime) } ;
sb.Completed += (s, ea) => m_collection.Remove (obj) ;
var fade = new DoubleAnimation (1, 0, new Duration (TimeSpan.FromMilliseconds (500))) ;
Storyboard.SetTarget (fade, view) ;
Storyboard.SetTargetProperty (fade, new PropertyPath (UIElement.OpacityProperty)) ;
sb.Children.Add (fade) ;
var size = new DoubleAnimation (view.ActualHeight, 0, new Duration (TimeSpan.FromMilliseconds (250))) ;
Storyboard.SetTarget (size, view) ;
Storyboard.SetTargetProperty (size, new PropertyPath (FrameworkElement.HeightProperty)) ;
sb.Children.Add (size) ;
size.BeginTime = fade.Duration.TimeSpan ;
sb.Begin () ;
}
}
private void OnViewCurrentChanged (object sender, EventArgs e)
{
m_sourceView.MoveCurrentTo (m_view.CurrentItem) ;
}
private void OnSourceCurrentChanged (object sender, EventArgs e)
{
m_view.MoveCurrentTo (m_sourceView.CurrentItem) ;
}
public ICollectionView View => m_view ;
}
}
Et les derniers mots. Tout d'abord, il fonctionne. Suivant-cette approche ne nécessite aucun changement dans le code existant, les solutions de contournement via la suppression de la propriété, etc., etc. Surtout lorsqu'il est implémenté en tant que contrôle personnalisé unique. Vous avez ObservableCollection, ajoutez des éléments, supprimez, faites ce que vous voulez, L'interface utilisateur essayera toujours de refléter correctement ces changements.