I\'ve been using this great article as a basis for showing and hiding elements with a transition effect. It works very neatly in that it lets you bind the Visibility
Present framework does something similar to this. Here is a demo of it. You can make use of it or do something similar with VisualStateManager.
Fade-in is easy, but for fade-out the items will need to stay in the source list until the animation is completed (like you said).
If we still want to be able to use the source ObservableCollection
normally (Add/Remove etc.) then we would have to create a mirror collection that is constantly in sync with the source collection with a delay for delete until the animation is completed. This can be done with the CollectionChanged
event.
Here is an implementation I made of this, using an attached behavior. It can be used for ItemsControl
, ListBox
, DataGrid
or anything else that derives from ItemsControl
.
Instead of Binding ItemsSource
, bind the attached property ItemsSourceBehavior.ItemsSource
. It will create a mirror ObservableCollection
using Reflection, use the mirror as ItemsSource
instead and handle the FadeIn/FadeOut
animations.
Note that I haven't tested this extensively and there might be bugs and several improvements that can be made but it has worked great in my scenarios.
Sample Usage
<ListBox behaviors:ItemsSourceBehavior.ItemsSource="{Binding MyCollection}">
<behaviors:ItemsSourceBehavior.FadeInAnimation>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0.0"
To="1.0"
Duration="0:0:3"/>
</Storyboard>
</behaviors:ItemsSourceBehavior.FadeInAnimation>
<behaviors:ItemsSourceBehavior.FadeOutAnimation>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.0"
Duration="0:0:1"/>
</Storyboard>
</behaviors:ItemsSourceBehavior.FadeOutAnimation>
<!--...-->
</ListBox>
ItemsSourceBehavior
public class ItemsSourceBehavior
{
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.RegisterAttached("ItemsSource",
typeof(IList),
typeof(ItemsSourceBehavior),
new UIPropertyMetadata(null, ItemsSourcePropertyChanged));
public static void SetItemsSource(DependencyObject element, IList value)
{
element.SetValue(ItemsSourceProperty, value);
}
public static IList GetItemsSource(DependencyObject element)
{
return (IList)element.GetValue(ItemsSourceProperty);
}
private static void ItemsSourcePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
ItemsControl itemsControl = source as ItemsControl;
IList itemsSource = e.NewValue as IList;
if (itemsControl == null)
{
return;
}
if (itemsSource == null)
{
itemsControl.ItemsSource = null;
return;
}
Type itemsSourceType = itemsSource.GetType();
Type listType = typeof(ObservableCollection<>).MakeGenericType(itemsSourceType.GetGenericArguments()[0]);
IList mirrorItemsSource = (IList)Activator.CreateInstance(listType);
itemsControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding{ Source = mirrorItemsSource });
foreach (object item in itemsSource)
{
mirrorItemsSource.Add(item);
}
FadeInContainers(itemsControl, itemsSource);
(itemsSource as INotifyCollectionChanged).CollectionChanged +=
(object sender, NotifyCollectionChangedEventArgs ne) =>
{
if (ne.Action == NotifyCollectionChangedAction.Add)
{
foreach (object newItem in ne.NewItems)
{
mirrorItemsSource.Add(newItem);
}
FadeInContainers(itemsControl, ne.NewItems);
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
foreach (object oldItem in ne.OldItems)
{
UIElement container = itemsControl.ItemContainerGenerator.ContainerFromItem(oldItem) as UIElement;
Storyboard fadeOutAnimation = GetFadeOutAnimation(itemsControl);
if (container != null && fadeOutAnimation != null)
{
Storyboard.SetTarget(fadeOutAnimation, container);
EventHandler onAnimationCompleted = null;
onAnimationCompleted = ((sender2, e2) =>
{
fadeOutAnimation.Completed -= onAnimationCompleted;
mirrorItemsSource.Remove(oldItem);
});
fadeOutAnimation.Completed += onAnimationCompleted;
fadeOutAnimation.Begin();
}
else
{
mirrorItemsSource.Remove(oldItem);
}
}
}
};
}
private static void FadeInContainers(ItemsControl itemsControl, IList newItems)
{
EventHandler statusChanged = null;
statusChanged = new EventHandler(delegate
{
if (itemsControl.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
itemsControl.ItemContainerGenerator.StatusChanged -= statusChanged;
foreach (object newItem in newItems)
{
UIElement container = itemsControl.ItemContainerGenerator.ContainerFromItem(newItem) as UIElement;
Storyboard fadeInAnimation = GetFadeInAnimation(itemsControl);
if (container != null && fadeInAnimation != null)
{
Storyboard.SetTarget(fadeInAnimation, container);
fadeInAnimation.Begin();
}
}
}
});
itemsControl.ItemContainerGenerator.StatusChanged += statusChanged;
}
public static readonly DependencyProperty FadeInAnimationProperty =
DependencyProperty.RegisterAttached("FadeInAnimation",
typeof(Storyboard),
typeof(ItemsSourceBehavior),
new UIPropertyMetadata(null));
public static void SetFadeInAnimation(DependencyObject element, Storyboard value)
{
element.SetValue(FadeInAnimationProperty, value);
}
public static Storyboard GetFadeInAnimation(DependencyObject element)
{
return (Storyboard)element.GetValue(FadeInAnimationProperty);
}
public static readonly DependencyProperty FadeOutAnimationProperty =
DependencyProperty.RegisterAttached("FadeOutAnimation",
typeof(Storyboard),
typeof(ItemsSourceBehavior),
new UIPropertyMetadata(null));
public static void SetFadeOutAnimation(DependencyObject element, Storyboard value)
{
element.SetValue(FadeOutAnimationProperty, value);
}
public static Storyboard GetFadeOutAnimation(DependencyObject element)
{
return (Storyboard)element.GetValue(FadeOutAnimationProperty);
}
}
@Fredrik Hedblad Nicely done. I do have a few remarks.
When adding an item the animation sometimes starts on the previously added item.
Inserting items into the list, added them all to the bottom (so no sorted list support)
In code below in have an addapted version, which resolves the issues listed above.
public class ItemsSourceBehavior
{
public static void SetItemsSource(DependencyObject element, IList value)
{
element.SetValue(ItemsSourceProperty, value);
}
public static IList GetItemsSource(DependencyObject element)
{
return (IList) element.GetValue(ItemsSourceProperty);
}
private static void ItemsSourcePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
//If animations need to be run together set this to 'false'.
const bool separateAnimations = true;
var itemsControl = source as ItemsControl;
var itemsSource = e.NewValue as IList;
if (itemsControl == null)
{
return;
}
if (itemsSource == null)
{
itemsControl.ItemsSource = null;
return;
}
var itemsSourceType = itemsSource.GetType();
var listType = typeof (ObservableCollection<>).MakeGenericType(itemsSourceType.GetGenericArguments()[0]);
var mirrorItemsSource = (IList) Activator.CreateInstance(listType);
itemsControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding {Source = mirrorItemsSource});
foreach (var item in itemsSource)
{
mirrorItemsSource.Add(item);
if (separateAnimations)
StartFadeInAnimation(itemsControl, new List<object> {item});
}
if (!separateAnimations)
{
StartFadeInAnimation(itemsControl, itemsSource);
}
(itemsSource as INotifyCollectionChanged).CollectionChanged +=
(object sender, NotifyCollectionChangedEventArgs ne) =>
{
if (ne.Action == NotifyCollectionChangedAction.Add)
{
foreach (var newItem in ne.NewItems)
{
//insert the items instead of just adding them
//this brings support for sorted collections
mirrorItemsSource.Insert(ne.NewStartingIndex, newItem);
if (separateAnimations)
{
StartFadeInAnimation(itemsControl, new List<object> {newItem});
}
}
if (!separateAnimations)
{
StartFadeInAnimation(itemsControl, ne.NewItems);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var oldItem in ne.OldItems)
{
var container = itemsControl.ItemContainerGenerator.ContainerFromItem(oldItem) as UIElement;
var fadeOutAnimation = GetFadeOutAnimation(itemsControl);
if (container != null && fadeOutAnimation != null)
{
Storyboard.SetTarget(fadeOutAnimation, container);
EventHandler onAnimationCompleted = null;
onAnimationCompleted = ((sender2, e2) =>
{
fadeOutAnimation.Completed -= onAnimationCompleted;
mirrorItemsSource.Remove(oldItem);
});
fadeOutAnimation.Completed += onAnimationCompleted;
fadeOutAnimation.Begin();
}
else
{
mirrorItemsSource.Remove(oldItem);
}
}
}
};
}
private static void StartFadeInAnimation(ItemsControl itemsControl, IList newItems)
{
foreach (var newItem in newItems)
{
var container = itemsControl.ItemContainerGenerator.ContainerFromItem(newItem) as UIElement;
var fadeInAnimation = GetFadeInAnimation(itemsControl);
if (container != null && fadeInAnimation != null)
{
Storyboard.SetTarget(fadeInAnimation, container);
fadeInAnimation.Begin();
}
}
}
public static void SetFadeInAnimation(DependencyObject element, Storyboard value)
{
element.SetValue(FadeInAnimationProperty, value);
}
public static Storyboard GetFadeInAnimation(DependencyObject element)
{
return (Storyboard) element.GetValue(FadeInAnimationProperty);
}
public static void SetFadeOutAnimation(DependencyObject element, Storyboard value)
{
element.SetValue(FadeOutAnimationProperty, value);
}
public static Storyboard GetFadeOutAnimation(DependencyObject element)
{
return (Storyboard) element.GetValue(FadeOutAnimationProperty);
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.RegisterAttached("ItemsSource",
typeof (IList),
typeof (ItemsSourceBehavior),
new UIPropertyMetadata(null, ItemsSourcePropertyChanged));
public static readonly DependencyProperty FadeInAnimationProperty =
DependencyProperty.RegisterAttached("FadeInAnimation",
typeof (Storyboard),
typeof (ItemsSourceBehavior),
new UIPropertyMetadata(null));
public static readonly DependencyProperty FadeOutAnimationProperty =
DependencyProperty.RegisterAttached("FadeOutAnimation",
typeof (Storyboard),
typeof (ItemsSourceBehavior),
new UIPropertyMetadata(null));
}