0

我正在编写一个自定义 ItemsControl 和 Panel,它按照将项目添加到绑定到 ItemsControl 的 ItemsSource 的顺序垂直排列项目。 请注意,这只是最终 Panel 的原型,其安排会更复杂一些。因此,我对备用小组建议不感兴趣。

ItemsControl 从绑定的集合中涓流馈送 Panel 项目,因此集合中的项目不会“同时”出现(Panel 引发一个事件表示它已准备好,ItemsControl 捕获该事件以释放下一个项目)。问题是,出于某种原因,面板上的 ArrangeOverride 有时会决定应将项目添加到已渲染的视觉效果的中间,从而导致东西跳来跳去。

目前,我只需单击测试视图上的“添加”按钮即可将项目添加到绑定的 ItemsSource 集合的末尾。因此,在发生这种涓涓馈送时,可以从绑定集合中添加/删除项目。当面板呈现这些“新”项目时,它们被添加到看似随机的位置。

Trace.Write在整个代码中都添加了 s,以便我可以看到项目已成功添加到集合的末尾,并确认 InternalChildren 被随机插入到中间。我什至实现了一个 CollectionViewSource 来强制对项目进行排序。即使在那时,InternalChildren 也对底层 ItemsSource 给出了不同的顺序。

我唯一能想到的是,在涓流喂养期间以某种方式添加项目会导致某种竞争条件,但这一切都在 UI 线程上,我仍然不明白为什么 ItemsControl 上的顺序是正确的,但不是面板上。

如何将 Panel 上的 InternalChildren 的顺序与绑定的 ItemsControl 同步,以便视觉对象以正确的顺序显示?

更新

根据要求,这里有一些代码。完整解决方案中有很多内容,因此我将尝试仅在此处发布相关位。因此,这段代码不会运行,但它应该给你一个想法。我已经删除了所有Trace.WriteLine代码,还有很多我认为对于解决手头问题并不重要的附加代码。

我有一个StaggeredReleaseCollection<T>which extends ObservableCollection<T>。添加到集合中的项目保存在单独的“HeldItems”集合中,直到它们准备好通过“Kick”方法(on IFlushableCollection)移动到继承的“Items”集合中。

public class StaggeredReleaseCollection<T> : ObservableCollection<T>, IFlushableCollection
    {
        public event EventHandler<PreviewEventArgs> PreviewKick;
        public event EventHandler HeldItemsEmptied;

        ExtendedObservableCollection<T> _heldItems;
        ReadOnlyObservableCollection<T> _readOnlyHeldItems;

        public StaggeredReleaseCollection()
        {
            //Initialise data
            _heldItems = new ExtendedObservableCollection<T>();
            _readOnlyHeldItems = new ReadOnlyObservableCollection<T>(_heldItems);

            _heldItems.CollectionChanged += (s, e) =>
            {
                //Check if held items is being emptied
                if (e.Action == NotifyCollectionChangedAction.Remove && !_heldItems.Any())
                {
                    //Raise event if required
                    if (HeldItemsEmptied != null) HeldItemsEmptied(this, new EventArgs());
                }
            };
        }

        /// <summary>
        /// Kick's the first held item into the Items collection (if there is one)
        /// </summary>
        public void Kick()
        {
            if (_heldItems.Any())
            {
                //Fire preview event
                if (PreviewKick != null)
                {
                    PreviewEventArgs args = new PreviewEventArgs();
                    PreviewKick(this, args);
                    if (args.IsHandled) return;
                }

                //Move held item to Items
                T item = _heldItems[0];
                _heldItems.RemoveAt(0);
                Items.Add(item);

                //Notify that an item was added
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
            }
        }
    }

我还有一个VerticalStackFlushPanel我正在构建的原型面板。该面板应将所有项目垂直放置在其表面上。添加项目时,Phase1 动画开始。完成后会引发一个事件,以便可以添加下一个项目。

public class VerticalStackFlushPanel : FlushPanel
{
    /// <summary>
    /// Layout vertically
    /// </summary>
    protected override Size MeasureOverride(Size availableSize)
    {
        Size desiredSize = new Size();
        for (int i = 0; i < InternalChildren.Count; i++)
        {
            UIElement uie = InternalChildren[i];
            uie.Measure(availableSize);
            desiredSize.Height += uie.DesiredSize.Height;
        }
        return desiredSize;
    }

    /// <summary>
    /// Arrange the child elements to their final position
    /// </summary>
    protected override Size ArrangeOverride(Size finalSize)
    {
        double top = 0d;
        for (int i = 0; i < InternalChildren.Count; i++)
        {
            UIElement uie = InternalChildren[i];
            uie.Arrange(new Rect(0D, top, finalSize.Width, uie.DesiredSize.Height));
            top += uie.DesiredSize.Height;
        }
        return finalSize;
    }

    public override void BeginPhase1Animation(DependencyObject visualAdded)
    {
        //Generate animation
        var da = new DoubleAnimation()
        {
            From = 0d,
            To = 1d,
            Duration = new Duration(TimeSpan.FromSeconds(1)),
        };

        //Attach completion handler
        AttachPhase1AnimationCompletionHander(visualAdded, da);

        //Start animation
        (visualAdded as IAnimatable).BeginAnimation(OpacityProperty, da);
    }

    public override void BeginPhase2Animation(DependencyObject visualAdded)
    {
        TextBlock tb = FindVisualChild<TextBlock>(visualAdded);
        if (tb != null)
        {
            //Generate animation
            var ca = new ColorAnimation(Colors.Red, new Duration(TimeSpan.FromSeconds(0.5)));
            SolidColorBrush b = new SolidColorBrush(Colors.Black);

            //Set foreground
            tb.Foreground = b;

            //Start animation
            b.BeginAnimation(SolidColorBrush.ColorProperty, ca);

            //Generate second animation
            AnimateTransformations(tb);
        }
    }
}

FlushPanel所基于的摘要VerticalStackFlushPanel处理阶段 1 动画事件的引发。出于某种原因,OnVisualChildrenChanged 在 Kick() 方法期间不会触发,StaggeredReleaseCollection除非我自己明确提出 OnCollectionChanged 事件(也许这是一个危险信号?)。

public abstract class FlushPanel : Panel
{

    /// <summary>
    /// An event that is fired when phase 1 of an animation is complete
    /// </summary>
    public event EventHandler<EventArgs<object>> ItemAnimationPhase1Complete;

    /// <summary>
    /// Invoked when the <see cref="T:System.Windows.Media.VisualCollection"/> of a visual object is modified.
    /// </summary>
    /// <param name="visualAdded">The <see cref="T:System.Windows.Media.Visual"/> that was added to the collection.</param>
    /// <param name="visualRemoved">The <see cref="T:System.Windows.Media.Visual"/> that was removed from the collection.</param>
    protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
    {
        base.OnVisualChildrenChanged(visualAdded, visualRemoved);

        if (visualAdded != null && visualAdded is IAnimatable) BeginPhase1Animation(visualAdded);
    }

    /// <summary>
    /// Begin an animation for Phase 1.  Use <seealso cref="AttachPhase1AnimationCompletionHander"/> to attach the completed event handler before the animation is started.
    /// </summary>
    /// <returns>An animation that can be used to determine Phase 1 animation is complete</returns>
    public abstract void BeginPhase1Animation(DependencyObject visualAdded);

    /// <summary>
    /// Generate an animation for Phase 2
    /// </summary>
    /// <returns>An animation that can be used to determine Phase 2 animation is complete</returns>
    public abstract void BeginPhase2Animation(DependencyObject visualAdded);

    /// <summary>
    /// Attaches an animation completion handler for the Phase 1 Animation that fires an event when the animation is complete.
    /// </summary>
    /// <remarks>
    /// This event is for when this panel is used on the <see cref="StaggeredReleaseItemsControl"/>, which uses it to kick the next item onto the panel.
    /// </remarks>
    public void AttachPhase1AnimationCompletionHander(DependencyObject visualAdded, AnimationTimeline animation)
    {
        if (animation != null) animation.Completed += (s, e) =>
        {
            //Raise event
            if (ItemAnimationPhase1Complete != null) ItemAnimationPhase1Complete(this, new EventArgs<object>(visualAdded));

            //Start next phase
            BeginPhase2Animation(visualAdded);
        };
    }
}

StaggeredReleaseItemsControl知道如何处理IFlushableCollectionFlushPanel(它StaggeredReleaseCollection<T>和基于VerticalStackFlushPanel)。如果它在运行时找到这些实例,它会协调将项目从 踢StaggeredReleaseCollection<T>VerticalStackFlushPanel,等待 Phase1 动画完成,然后踢下一个项目,等等。

它通常会阻止新项目在 Phase1 动画结束之前被踢出,但是我禁用了该部分以加快测试速度。

public class StaggeredReleaseItemsControl : ItemsControl
{
    FlushPanel _flushPanel;
    IFlushableCollection _collection;

    /// <summary>
    /// A flag to track when a Phase 1 animation is underway, to prevent kicking new items
    /// </summary>
    bool _isItemAnimationPhase1InProgress;

    static StaggeredReleaseItemsControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(StaggeredReleaseItemsControl), new FrameworkPropertyMetadata(typeof(StaggeredReleaseItemsControl)));
    }

    public override void OnApplyTemplate()
    {
        _flushPanel = FindVisualChild<FlushPanel>(this);
        if (_flushPanel != null)
        {
            //Capture when Phase 1 animation is completed
            _flushPanel.ItemAnimationPhase1Complete += (s, e) =>
            {
                _isItemAnimationPhase1InProgress = false;

                //Kick collection so next item falls out (and starts it's own Phase 1 animation)
                if (_collection != null) _collection.Kick();
            };
        }
        base.OnApplyTemplate();
    }

    protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);

        //Grab reference to collection
        if (newValue is IFlushableCollection)
        {
            //Grab collection
            _collection = newValue as IFlushableCollection;

            if (_collection != null)
            {
                //NOTE:
                //Commented out to speed up testing
                ////Capture preview kick event
                //_collection.PreviewKick += (s, e) =>
                //{
                //    if (e.IsHandled) return;

                //    //Swallow Kick if there is already a Phase 1 animation in progress
                //    e.IsHandled = _isItemAnimationPhase1InProgress;

                //    //Set flag
                //    _isItemAnimationPhase1InProgress = true;
                //};

                //Capture held items empty event
                _collection.HeldItemsEmptied += (s, e) =>
                {
                    _isItemAnimationPhase1InProgress = false;
                };

                //Kickstart (if required)
                if (AutoKickStart) _collection.Kick();

            }
        }
    }
}

}

Generic.xaml文件组合了一个标准模板。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:si="clr-namespace:AnimatedQueueTest2010.StaggeredItemControlTest.Controls"
>
    <!--StaggeredReleaseItemControl Style-->
    <Style TargetType="{x:Type si:StaggeredReleaseItemsControl}" BasedOn="{StaticResource {x:Type ItemsControl}}">
        <Setter Property="FontSize" Value="20" />
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <si:VerticalStackFlushPanel/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

我的测试视图相当简单。

<Window 
    x:Class="AnimatedQueueTest2010.StaggeredItemControlTest.Views.StaggeredItemControlTestView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:AnimatedQueueTest2010.StaggeredItemControlTest.Controls"
    xmlns:cm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
    Title="StaggeredItemControlTestView" 
    Width="640" Height="480" 
>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <local:StaggeredReleaseItemsControl x:Name="ic" ItemsSource="{Binding ViewModel.Names}" />

        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <StackPanel.Resources>
                <Style TargetType="Button">
                    <Setter Property="MinWidth" Value="80"/>
                    <Setter Property="MinHeight" Value="20"/>
                </Style>
            </StackPanel.Resources>
            <Button x:Name="btnKick" Content="Kick" Click="btnKick_Click"/>
            <Button x:Name="btnAdd" Content="Add" Click="btnAdd_Click"/>
        </StackPanel>

    </Grid>

</Window>

我的 ViewModel 定义了初始状态。

public class StaggeredItemControlTestViewModel : INotifyPropertyChanged
{
    public StaggeredReleaseCollection<string> Names { get; set; }

    public StaggeredItemControlTestViewModel()
    {
        Names = new StaggeredReleaseCollection<string>() { "Carl", "Chris", "Sam", "Erin" };
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

后面的代码是供我与之交互的。

public partial class StaggeredItemControlTestView : Window
{
    List<string> GenesisPeople = new List<string>() { "Rob", "Mike", "Cate", "Andrew", "Dave", "Janet", "Julie" };
    Random random = new Random((int)(DateTime.Now.Ticks % int.MaxValue));

    public StaggeredItemControlTestViewModel ViewModel { get; set; }

    public StaggeredItemControlTestView()
    {
        InitializeComponent();
        ViewModel = new StaggeredItemControlTestViewModel();
        DataContext = this;
    }

    private void btnKick_Click(object sender, RoutedEventArgs e)
    {
        ViewModel.Names.Kick();
    }

    private void btnAdd_Click(object sender, RoutedEventArgs e)
    {
        //Get a random name
        //NOTE: Use a new string here to ensure it's not reusing the same object pointer
        string nextName = new string(GenesisPeople[random.Next(GenesisPeople.Count)].ToCharArray());

        //Add to ViewModel
        ViewModel.Names.Add(nextName);
    }
}

当它运行时,我点击了几次“添加”按钮,然后点击了几次“踢”按钮,依此类推。正如我之前所说,这些收藏品正在以正确的顺序涓涓细流。但是,在 期间ArrangeOveride,InternalChildren 集合偶尔会将新添加的项目报告为位于集合的中间而不是末尾。鉴于通常一次只添加一个项目,我不明白为什么会这样。

为什么 Panel 上的 InternalChildren 显示的顺序与 bound 不同StaggeredReleaseCollection<T>

4

1 回答 1

0

尤里卡!多亏了 Clemens 的探索,我发现了问题所在。

这个问题与我为什么要提高OnCollectionChanged自己有关。我StaggeredReleaseCollection在此集合上定义了 Add() 的新定义,以便将项目添加到我持有的集合中(而不是 ObservableCollection 上的基础 Items 集合)。在 Kick() 期间,我Items.Add(item)用来将项目从我持有的集合移动到基础集合。

解决方案是改为调用base.Add(item)。使用 Reflector 我可以看到 base.Add(item) 已打开Collection<T>并且Items.Add()基于IList<T>. 所以只base.Add()包含我在这个解决方案中依赖的所有通知属性。

在旁边

我开始怀疑是否有更好的方法是让小组自行控制这一切。如果我允许项目以正常方式累积,也许我可以向视觉效果添加一些属性,以便面板可以监视阶段 1 动画的完成并重新安排下一个项目。

我猜这是我必须探索的东西。

于 2012-11-01T07:59:11.673 回答