Проведя безумные часы, охотясь на дебри Google, я думаю, что должен поделиться тем, как я решил эту проблему, так как она кажется довольно простой вещью, которая нужна, и все же WPF делает это до смешного разочаровывающим, пока вы не поймете, как анимация реализовано. Как только вы это сделаете, вы понимаете, что FrameworkElement.Unloaded - бесполезное событие для анимации. Я видел много версий этого вопроса по всему StackOverflow (среди прочих), со всевозможными хакерскими способами решить эту проблему. Надеюсь, я смогу привести самый простой пример, который вы можете придумать для своих многочисленных целей.
Я не буду показывать Fade In пример, поскольку он покрыт множеством примеров, уже использующих перенаправленное событие Loaded. Это затухание при удалении предметов, что является королевской болью в * @ $.
Основная проблема здесь связана с тем, что раскадровки становятся странными, когда вы помещаете их в элементы управления / шаблоны данных / стили. Невозможно привязать DataContext (и, следовательно, идентификатор вашего объекта) к раскадровке. Событие Completed начинается с нулевого представления о том, кем оно только что закончилось. Погружение в визуальное дерево бесполезно, поскольку все элементы шаблона данных имеют одинаковые имена для своих контейнеров! Конечно, вы могли бы написать функцию, которая будет выполнять поиск во всей коллекции объектов, для которых установлено свойство флажка удаления, но это уродливо и честно, просто не то, что вы когда-либо хотели бы разрешить писать намеренно. И это не сработает, если у вас есть несколько объектов, удаляемых в пределах анимации друг друга (это мой случай). Вы также можете просто написать поток очистки, который делает подобные вещи и теряется во времени. Не весело. Я отвлекся. На решение.
Предположения:
- Вы используете ObservableCollection, заполненную некоторыми пользовательскими объектами
- Вы используете DataTemplate, чтобы придать им индивидуальный вид, поэтому вы хотите анимировать их удаление
- Вы связываете ObservableCollection с ListBox (или с чем-то простым)
- У вас есть INotifyPropertyChanged, реализованный для класса объектов в вашем OC.
Тогда решение очень простое, на самом деле, мучительно, так что если вы потратили много времени на его решение.
Создайте раскадровку, которая оживляет ваше затухание в разделе Window.Resources вашего окна (над шаблоном DataTemplate).
(Необязательно) Определите Duration отдельно как ресурс, чтобы вы могли избежать жесткого кодирования. Или просто жесткий код длительности.
Создайте в вашем объектном классе открытое логическое свойство с именами «Removing», «isRemoving», whatev. Убедитесь, что вы подняли событие Property Changed для этого поля.
Создайте DataTrigger, который привязывается к вашему свойству «Удаление» и в режиме True воспроизводит раскадровку с затуханием.
Создайте частный объект 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>
ура! ~