WPF: Showing and hiding items in an ItemsControl with effects

穿精又带淫゛_ 提交于 2019-12-30 04:44:11

问题


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.


回答1:


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);
    }
}



回答2:


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



回答3:


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.



来源:https://stackoverflow.com/questions/6747491/wpf-showing-and-hiding-items-in-an-itemscontrol-with-effects

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!