自定义面板InternalChildren订单与绑定的ItemsSource不同

时间:2012-10-29 07:03:46

标签: wpf panel itemscontrol itemssource

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

ItemsControl从绑定集合中逐步提供Panel项目,因此集合中的项目并非全部“同时”出现(Panel会引发一个事件,表明它已准备就绪,ItemsControl会捕获该事件以释放下一个项目)。问题在于,由于某些原因,Panel上的ArrangeOverride有时会决定应该在已经渲染的视觉效果的中间添加项目,从而导致事物跳转。

目前,我只是单击测试视图上的“添加”按钮,将项目添加到绑定的ItemsSource集合的末尾。因此,当这种涓流喂食发生时,可以从绑定的集合中添加/删除项目。当小组提交这些“新”项目时,它们将被添加到看似随机的位置。

我的代码遍布Trace.Write,以便我可以看到项目已成功添加到集合的末尾,并确认InternalChildren是在中间随机插入的。我甚至实现了CollectionViewSource以强制执行项目的顺序。即使这样,InternalChildren也会给基础的ItemsSource提供不同的订单。

我唯一能想到的是,在涓流喂食期间以某种方式添加物品会导致某种竞争条件,但这一切都在UI线程中,我仍然无法理解为什么订单在ItemsControl但不在Panel上。

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

更新

根据要求,这里有一些代码。在完整的解决方案中有很多,所以我会尝试只在这里发布相关的位。因此,此代码将无法运行,但它应该给你一个想法。我删除了所有Trace.WriteLine代码,而且我认为很多其他代码对于解决手头的问题并不重要。

我的StaggeredReleaseCollection<T>扩展了ObservableCollection<T>。添加到集合中的项目保存在单独的“HeldItems”集合中,直到它们准备好通过“Kick”方法(在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知道如何处理IFlushableCollectionFlushPanelStaggeredReleaseCollection<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显示与绑定StaggeredReleaseCollection<T>不同的顺序?

1 个答案:

答案 0 :(得分:0)

尤里卡!由于克莱门斯的探测,我发现了这个问题。

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

解决方案是改为呼叫base.Add(item)。使用Reflector我可以看到base.Add(item)在Collection<T>上,Items.Add()基于IList<T>。所以只有base.Add()包含我在此解决方案中依赖的所有通知属性。

<强>除了

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

这是我想要探索的东西。