0

ItemsControl にバインドされた ItemsSource に追加された順序で項目を垂直に配置するカスタム ItemsControl と Panel を作成しています。 これは最終的な Panel の単なるプロトタイプであり、その配置はもう少し複雑になることに注意してください。そのため、別のパネルの提案には興味がありません。

ItemsControl はバインドされたコレクションから Panel 項目をトリクル フィードするため、コレクション内のすべての項目が「同時に」表示されるわけではありません (Panel は準備ができたことを示すイベントを発生させ、ItemsControl はそれをキャプチャして次の項目を解放します)。問題は、何らかの理由で Panel の ArrangeOverride が、既にレンダリングされたビジュアルの途中にアイテムを追加する必要があると判断することがあり、物事がジャンプすることです。

現時点では、テスト ビューの [追加] ボタンをクリックして、バインドされた ItemsSource コレクションの末尾に項目を追加するだけです。そのため、このトリクル フィーディングが発生している間に、バインドされたコレクションにアイテムを追加/削除できます。パネルがこれらの「新しい」アイテムをレンダリングするまでに、一見ランダムな場所に追加されています。

Trace.Writeアイテムがコレクションの最後に正常に追加されたことを確認できるように、また、InternalChildren が途中でランダムに挿入されていることを確認できるように、コード全体にs があります。CollectionViewSource を実装してアイテムの順序を強制するところまで行きました。それでも、InternalChildren は基になる ItemsSource に別の順序を与えていました。

私が考えることができる唯一のことは、トリクルフィード中に何らかの形でアイテムを追加すると、何らかの競合状態が発生することですが、それはすべてUIスレッド上にあり、ItemsControlで順序が正しい理由をまだ理解できません。パネルで。

ビジュアルが正しい順序で表示されるように、パネル上の InternalChildren の順序をバインドされた ItemsControl と同期するにはどうすればよいですか?

アップデート

リクエストに応じて、ここにいくつかのコードがあります。完全なソリューションにはそれがたくさんあるので、関連するビットのみをここに投稿しようとします。そのため、このコードは実行されませんが、アイデアが得られるはずです。すべてのTrace.WriteLineコードを削除しました。追加のコードの多くは、目前の問題を解決するのに重要ではないと思います。

StaggeredReleaseCollection<T>拡張するがありますObservableCollection<T>。コレクションに追加されたアイテムは、「Kick」メソッド ( on ) によって継承された「Items」コレクションに移動する準備が整うまで、別の「HeldItems」コレクションに保持されIFlushableCollectionます。

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 アニメーション イベントの発生を処理します。なんらかの理由で、StaggeredReleaseCollection自分で OnCollectionChanged イベントを明示的に発生させない限り、Kick() メソッド中に OnVisualChildrenChanged が起動しません (これは危険信号でしょうか?)。

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

はandStaggeredReleaseItemsControlを処理する方法を知っています(どれとに基づいています)。実行時にこれらのインスタンスが見つかった場合、 からへのキック アイテムを調整し、フェーズ 1 アニメーションが完了するのを待ってから、次のアイテムをキックします。IFlushableCollectionFlushPanelStaggeredReleaseCollection<T>VerticalStackFlushPanelStaggeredReleaseCollection<T>VerticalStackFlushPanel

通常、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 コレクションでは、新しく追加されたアイテムがコレクションの最後ではなく途中にあると報告されることがあります。通常、アイテムは一度に 1 つしか追加されないことを考えると、なぜそうなのか理解できません。

Panel の InternalChildren が bind とは異なる順序を示しているのはなぜStaggeredReleaseCollection<T>ですか?

4

1 に答える 1

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()、このソリューションで信頼しているすべての通知プロパティの良さのみが含まれています。

脇白

パネルがこれらすべてを自分で制御できるようにするのがより良い方法であるかどうか疑問に思い始めています。アイテムを通常の方法で蓄積できるようにすると、パネルがフェーズ1アニメーションの完了を監視し、次のアイテムを再配置できるように、ビジュアルにいくつかのプロパティを追加できる可能性があります。

それは私が推測しなければならないことです。

于 2012-11-01T07:59:11.673 に答える