WPF 实现已绑定集合项目的移除时动画过渡

信不信由你,这个场景貌似没有官方的完美解决方案。我认为这个要求一点都不过分,花了很长时间 bai google du,就是没找到好的方案。这是我花了一天时间才搞通的一个方案,跟大家分享,虽然仍然不太完美,但是希望对大家有用。

对完美的憧憬

一个已绑定到 ItemsControl 的集合,是不能通过 ItemsControl.Items 属性来访问的,如果你这样做,就会在运行时收到 InvalidOperationException。事实上,我们也不应该关心数据具体绑定到什么样的 ItemsControl 上,要删除数据,只需在 ObservableCollection 上进行 Remove 操作就可以了,然后在绑定数据的容器(例如 ListBoxItem)上收到 Unloading 事件,我们再在容器的 Style 里加一个 EventTrigger 写一段动画,这个过程中,ObservableCollection 认为数据已经删除,但 ItemsControl 仍然将数据保留到事件处理结束,这样就一切完美了。

但是在现阶段,只要绑定的集合删除一项数据,该项数据的容器会被立马删除,然后才姗姗来迟地激发 Unloaded 事件,这事件对于要做动画过渡的要求已经为时过晚,容器死不能复生了。

我的方案

1、切入点

既然在绑定源集合上删除数据会导致对应 UI 容器被立即删除,那么就不能上来就用 Remove 方法了,应该让这个方法在过渡动画完成后才执行—— 缺点1,Dev 需要知道绑定的 UI 容器支持移除时动画过渡(欢迎讨论自动检测的方法)。这里我选择使用扩展方法:

public static class ItemAnimatingRemover
{
    public static void RemoveWithAnimation<TData>( this ICollection<TData> dataSource, TData item )
        where TData : DependencyObject
    {
    }

TData 需要为 DependencyObject,为了能在 WPF 中绑定,相信这个要求不过分。

2、标记为要删除的数据项

总不能在我们的数据项里面每个都加上一个 bool 属性吧?这样就太大牺牲了,我选择使用 Attached Property。

public static readonly DependencyProperty RemovalAnimationBeginProperty =
    DependencyProperty.RegisterAttached(
        "RemovalAnimationBegin", typeof(bool), typeof(ItemAnimatingRemover));

private static void SetRemovalAnimationBegin( DependencyObject obj )
{
    obj.SetValue(RemovalAnimationBeginProperty, true);
}

3、要交给动画系统了,得找个地方寄存一下集合源

如果人家觉得看动画不耐烦了,再触发一次,就直接删除算了。

static Hashtable itemsToRemove = new Hashtable();

public static void RemoveWithAnimation<TData>( this ICollection<TData> dataSource, TData item )
    where TData : DependencyObject
{
    if ( itemsToRemove.Contains(item) )
    {
        dataSource.Remove(item);
        itemsToRemove.Remove(item);
    }
    itemsToRemove.Add((DependencyObject)item, dataSource);
    SetRemovalAnimationBegin(item);
}

这样 RemoveWithAnimation 方法就写完了,剩下的事由动画系统处理,动画完了,我们再来 Remove,所以需要暂存一下我们的 dataSource,使用 Hashtable 的原因是,TData 不确定,XAML 对泛型的支持也不好,所以,动画完了回来后,不预先保存是会丢失的。下面来看我们怎么在 XAML 中处理。

4、XAML 中响应 RemovalAnimationBegin Attached Property

这事相当需要技巧,因为现阶段 XAML 要在 ControlTemplate.Triggers 中设置自定义的 Attached Property Trigger 恐怕只有以下这一种写法能在运行时生效(文档骗人的555),假设 Designer 已经做好一个叫 ItemRemove 的 Storyboard:

<Style x:Key="DefaultItemContainerStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
<ControlTemplate.Triggers>
    <DataTrigger Binding="{Binding Path=(t:ItemAnimatingRemover.RemovalAnimationBegin)}" Value="true">
        <DataTrigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource ItemRemove}"/>
        </DataTrigger.EnterActions>
    </DataTrigger>

要点:1、要用 DataTrigger;2、Binding 一定要写 Path=

现在,你可以玩一下删除动画了,但是动画完成了以后,数据项并没有真正被删除掉啊!只是从 UI 上失踪而已,我们要把它彻底清除掉。

5、动画完成以后……

动画完成后 Storyboard 的 Complete 事件会激发,so, OnComplete = …? 不行。MSDN Library 告诉我们,不能在 XAML 为 Storyboard 定义 EventHandler(看 Animate in a Style 部分最后一条),原因也很简单,因为 Completed 不是 RoutedEvent。What about an EventTrigger? 也不行。问题在于你不能自定义 TriggerAction,尽管 TriggerAction 是公共的抽象类,但是要重写的 Invoke 方法却被修饰为 internal,虽然能用 IL 重写,但是…… 再者,Storyboard 也没有 Triggers 集合让我们添加 Trigger 呀。

既然我们不能处理 Complete 事件,那么就创造一个 Complete 属性通知代码好了,更直接。

public static readonly DependencyProperty RemovalAnimationCompleteProperty =
    DependencyProperty.RegisterAttached(
        "RemovalAnimationComplete", typeof(bool), typeof(ItemAnimatingRemover),
        new PropertyMetadata(OnRemovalAnimationComplete));

等下再给出 OnRemovalAnimationComplete 的实现,现在看 XAML 怎么在动画完成时触发 RemovalAnimationComplete Attached Property:

<Storyboard x:Key="ItemRemove">
    <!-- Omitted codes generated by tools -->
    <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(t:ItemAnimatingRemover.RemovalAnimationComplete)">
        <DiscreteBooleanKeyFrame KeyTime="00:00:00.4500000" Value="True"></DiscreteBooleanKeyFrame>
    </BooleanAnimationUsingKeyFrames>
</Storyboard>

假如这个过渡动画需要 0.4 秒完成,那么我们在 0.45 秒的地方手动插入一个关键帧,设置 RemovalAnimationComplete 属性为 true。—— 缺点2,每种过渡动画都要算一下过渡时间,而且过渡时间变了就要重新改一次(还是事件好啊!求事件解法……)。

6、清理

现在我们实现 OnRemovalAnimationComplete 方法,注意上面 XAML 设置的 Attach Property 目标是装数据的容器(如 ListBoxItem)。

static void OnRemovalAnimationComplete( DependencyObject container, DependencyPropertyChangedEventArgs e )
{
    if ( !(container is ContentControl) )
    {
        return;
    }
    var data = ((ContentControl)container).Content;
    if ( !itemsToRemove.Contains(data) || (bool)e.NewValue == false )
    {
        return;
    }
    var dataSource = itemsToRemove[data];
    dataSource.GetType().GetMethod("Remove").Invoke(dataSource, new object[] { data });
    itemsToRemove.Remove(data);
}

TData 类型在这个方法在编译时已经丢失,只能用反射调用了。

总结&感想

有些东西啊,对外声称很强大,在公开的 Demo 中也确实用很少的工作量完成了一个个很炫的功能,但是并不是所有东西都一下子能完美实现的。不能被这些光环冲昏了头脑,认为 XYZ 是万能的,这里的 WPF 就是一个例子。最后,我的方案不是很完美,主要的两个缺点我在文章中都列举出来了,希望有兴趣研究的朋友一起讨论一下,也许大家有更好的方法。

完整的例子下载

posted @ 2009-11-10 13:42 DiryBoy 阅读(...) 评论(...) 编辑 收藏