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
property just as normal, then define what happens when the visibility changes (e.g. animate its opacity or trigger a storyboard). When you hide an element, it uses value coercion to keep it visible until the transition is finished.
I'm looking for a similar solution to use with an ItemsControl
and an ObservableCollection
. In other words, I want to bind the ItemsSource
to an ObservableCollection
as normal, but control what happens when items are added and removed and trigger animations. I don't think using value coercion will work here, but obviously, items still need to stay in the list until their transitions finish. Does anyone know of any existing solutions that would make this easy?
I'd like any solution to be reasonably generic and easy to apply to lists of any kind of items. Ideally the style and animation behaviour would be separate, and applying it to a particular list would be a simple task such as giving it an attached property.
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));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With