WPF: Showing and hiding items in an ItemsControl with effects
Asked Answered
T

3

7

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.

Terresaterrestrial answered 19/7, 2011 at 12:55 Comment(1)
i have solution which will work for adding items... i am also looking for removing items thing...Means
A
8

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);
    }
}
Adze answered 31/7, 2011 at 4:1 Comment(8)
This is a great solution! There was one small bug when starting the fade-out animation: the event handlers were not being removed from the storyboard so kept piling up resulting in multiple items being removed when one was removed. I've edited your answer and fixed the issue.Terresaterrestrial
I'd got as far as the mirrored list previously but couldn't figure out how to attach the storyboard to items. My solution involved coercing the ItemsSource property to generate and return the mirrored list. That way you can continue using the same old ItemsSource property and set storyboards using styles. I'll post that solution here too if I get it working. Thanks Meleak.Terresaterrestrial
@Tim Rogers: Coercing the ItemsSource property sounds great, will be interesting to see if you get it working! I'm also playing around with a move animation for 'NotifyCollectionChangedAction.Move', I'll update here as well if I manage to put something togetherAdze
@Tim Rogers - this is just the thing I'm looking for. I'm just having trouble getting this working - would you have a sample project to show this more fully? Thanks.Dijon
It only began to work for me when I removed everything but foreach loop from FadeInContainers.Cowberry
I updated the code so that the call to FadeInContainers happens before the items are added to the databound list as I was finding that the events that FadeInContainers was setting up to handle were being triggered immediately after the databound list was updated and therefore you were getting the previously added item faded in. This seems to be only an issue with ItemsControl and not a ListBox control.Hudnut
Clone the Storyboard in the fadeout otherwise when 2 or more containers are being faded at the same time all Completed handlers will run when the first one Completes. Storyboard fadeOutAnimation = GetFadeOutAnimation(itemsControl).Clone();Delusion
Nice idea ! But the code does not work as expected (new Items are not faded in, instead older items are faded, fade out not working if multiple items are in the collection, and CollectionChanged event is never removed?). I'm using datatemplates ...Superorder
O
1

@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)

  • (personal issue: needed separate animation for each item)

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));
}
Ovid answered 21/1, 2016 at 10:58 Comment(0)
F
0

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.

Fanti answered 19/7, 2011 at 16:18 Comment(1)
Similar, but it doesn't help me bind to an ObservableCollection and hook onto its events as far as I can see.Terresaterrestrial

© 2022 - 2024 — McMap. All rights reserved.