Как я могу реализовать добавление и удаление добавленных / удаленных ListItems - PullRequest
27 голосов
/ 13 ноября 2010

Предположим, у меня ListBox привязано к ObservableCollection, и я хочу анимировать добавление / удаление ListBoxItems, например.FadeIn / Out, SlideDown / Up и т. Д. Как я могу это сделать?

Ответы [ 7 ]

30 голосов
/ 31 января 2013

Проведя безумные часы, охотясь на дебри Google, я думаю, что должен поделиться тем, как я решил эту проблему, так как она кажется довольно простой вещью, которая нужна, и все же WPF делает это до смешного разочаровывающим, пока вы не поймете, как анимация реализовано. Как только вы это сделаете, вы понимаете, что FrameworkElement.Unloaded - бесполезное событие для анимации. Я видел много версий этого вопроса по всему StackOverflow (среди прочих), со всевозможными хакерскими способами решить эту проблему. Надеюсь, я смогу привести самый простой пример, который вы можете придумать для своих многочисленных целей.

Я не буду показывать Fade In пример, поскольку он покрыт множеством примеров, уже использующих перенаправленное событие Loaded. Это затухание при удалении предметов, что является королевской болью в * @ $.

Основная проблема здесь связана с тем, что раскадровки становятся странными, когда вы помещаете их в элементы управления / шаблоны данных / стили. Невозможно привязать DataContext (и, следовательно, идентификатор вашего объекта) к раскадровке. Событие Completed начинается с нулевого представления о том, кем оно только что закончилось. Погружение в визуальное дерево бесполезно, поскольку все элементы шаблона данных имеют одинаковые имена для своих контейнеров! Конечно, вы могли бы написать функцию, которая будет выполнять поиск во всей коллекции объектов, для которых установлено свойство флажка удаления, но это уродливо и честно, просто не то, что вы когда-либо хотели бы разрешить писать намеренно. И это не сработает, если у вас есть несколько объектов, удаляемых в пределах анимации друг друга (это мой случай). Вы также можете просто написать поток очистки, который делает подобные вещи и теряется во времени. Не весело. Я отвлекся. На решение.

Предположения:

  1. Вы используете ObservableCollection, заполненную некоторыми пользовательскими объектами
  2. Вы используете DataTemplate, чтобы придать им индивидуальный вид, поэтому вы хотите анимировать их удаление
  3. Вы связываете ObservableCollection с ListBox (или с чем-то простым)
  4. У вас есть INotifyPropertyChanged, реализованный для класса объектов в вашем OC.

Тогда решение очень простое, на самом деле, мучительно, так что если вы потратили много времени на его решение.

  1. Создайте раскадровку, которая оживляет ваше затухание в разделе Window.Resources вашего окна (над шаблоном DataTemplate).

  2. (Необязательно) Определите Duration отдельно как ресурс, чтобы вы могли избежать жесткого кодирования. Или просто жесткий код длительности.

  3. Создайте в вашем объектном классе открытое логическое свойство с именами «Removing», «isRemoving», whatev. Убедитесь, что вы подняли событие Property Changed для этого поля.

  4. Создайте DataTrigger, который привязывается к вашему свойству «Удаление» и в режиме True воспроизводит раскадровку с затуханием.

  5. Создайте частный объект DispatcherTimer в своем классе объектов и реализуйте простой таймер, который имеет ту же продолжительность, что и анимация исчезновения, и удаляет ваш объект из списка в его обработчике тиков.

Ниже приведен пример кода, который, надеюсь, облегчает понимание. Я максимально упростил пример, поэтому вам нужно будет адаптировать его к своей среде так, как вам удобно.

Код позади

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>

ура! ~

19 голосов
/ 16 ноября 2010

Доктор ТиДжей достаточно прав.Пройдя по этому маршруту, вы должны будете обернуть ObservableCollection<T> и реализовать событие BeforeDelete, ... тогда вы можете использовать EventTrigger для управления раскадровками.

Хотя это правильная боль.Возможно, вам лучше создать DataTemplate и обработать события FrameworkElement.Loaded и FrameworkElement.Unloaded в EventTrigger.

Я собрал краткий образец для вас ниже.Вам придется разобраться с кодом удаления самостоятельно, но я уверен, что вы готовы.

    <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

2 голосов
/ 19 ноября 2010

Исчезновение, вероятно, будет невозможно без переписывания базовой реализации ItemsControl.Проблема в том, что когда ItemsControl получает событие INotifyCollectionChanged из коллекции, оно немедленно (и в глубоком закрытом коде) помечает контейнер элемента как невидимый (IsVisible является свойством только для чтения, которое получает свое значение из скрытого кэшапоэтому недоступен).

Вы можете легко реализовать постепенное добавление следующим образом:

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

Но эквивалент постепенного исчезновения никогда не работает, так как контейнер уже невидим ине может быть сброшено.

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

Даже если у вас есть собственный генератор контейнеров, вы не сможете решить эту проблему

protected override DependencyObject GetContainerForItemOverride()
    {
        return new FadingListBoxItem();
    }

И этот вид имеет смысл, потому что если контейнер все еще былвидимые после того, как данные, которые они представляют, исчезли, тогда вы можете теоретически щелкнуть контейнер (запустить триггеры, события и т. д.) и, возможно, столкнуться с некоторыми тонкими ошибками.

1 голос
/ 20 июля 2015

Принятый ответ работает для анимации добавления новых элементов, но не для удаления существующих. Это связано с тем, что к моменту возникновения события Unloaded элемент уже был удален. Ключом к удалению на работу является добавление понятия «помечено для удаления». Пометка для удаления должна вызвать анимацию, а завершение анимации должно вызвать фактическое удаление. Вероятно, существует множество способов реализации этой идеи, но я заставил ее работать, создав привязанное поведение и немного подправив свои модели представления. Поведение предоставляет три прикрепленных свойства, каждое из которых должно быть установлено для каждого ListViewItem:

  1. «Раскадровка» типа Storyboard. Это фактическая анимация, которую вы хотите запустить при удалении элемента.
  2. «PerformRemoval» типа ICommand. Это команда, которая будет выполнена после завершения анимации. Он должен выполнить код для фактического удаления элемента из коллекции данных.
  3. «IsMarkedForRemoval» типа bool. Установите для этого параметра значение true, если вы решили удалить элемент из списка (например, в обработчике нажатия кнопки). Как только присоединенное поведение увидит, что это свойство изменилось на true, оно начнет анимацию. И когда сработает событие Completed анимации, оно будет Execute командой PerformRemoval.

Здесь - это ссылка на полный исходный код поведения и примера использования (если отправлять ссылку на свой блог неправильно, я удалю ссылку. Я вставлю код сюда, но это довольно долго. Я не получаю деньги от вещи, если это имеет значение).

0 голосов
/ 26 декабря 2017

Хех.Поскольку принятое решение не работает, давайте попробуем еще один раунд;)

Мы не можем использовать событие Unloaded, потому что ListBox (или другой элемент управления) удаляет элемент из визуального дерева, когда он удаляется из исходного списка.Поэтому основная идея - создать теневую копию предоставленной коллекции ObservableCollection и связать с ней список.

Прежде всего - XAML:

<ListBox ItemsSource="{Binding ShadowView}" IsSynchronizedWithCurrentItem="True">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border Loaded="OnItemViewLoaded">
                <TextBlock Text="{Binding}"/>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Создать ListBox, привязать его к нашей теневой копии, установить IsSynchronizedWithCurrentItemдля правильной поддержки ICollectionView.CurrentItem (очень полезный интерфейс) и установите событие Loaded в представлении элемента.Этот обработчик событий должен связать представление (которое будет анимировано) и элемент (который будет удален).

private void OnItemViewLoaded (object sender, RoutedEventArgs e)
{
    var fe = (FrameworkElement) sender ;
    var dc = (DependencyObject) fe.DataContext ;

    dc.SetValue (ShadowViewSource.ViewProperty, fe) ;
}

Инициализировать все:

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

И последнее, но не менее важное:Класс ShadowViewSource (да, он не идеален, но как доказательство концепции работает):

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

И последние слова.Прежде всего это работает.Далее - этот подход не требует каких-либо изменений в существующем коде, обходных путей с помощью свойства «Удаление» и т. Д. И т. Д. И т. Д., Особенно при реализации в качестве единого пользовательского элемента управления.Вы имеете ObservableCollection, добавляете элементы, удаляете, делаете все, что хотите, пользовательский интерфейс всегда будет стараться корректно отразить эти изменения.

0 голосов
/ 14 мая 2012

Для меня FrameworkElement.Unloaded событие не работает - предмет просто исчезает мгновенно.Я с трудом могу поверить, что многолетний опыт работы с WPF не принес ничего более красивого, но похоже, что единственный способ, которым это может работать, - это хак, описанный здесь: Анимация удаленного элемента в списке ? ..

0 голосов
/ 13 ноября 2010

Создайте две раскадровки для постепенного появления и исчезновения и свяжите их значение с кистью, которую вы создали для OpacityMask вашего ListBox

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...