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 アニメーションが完了するのを待ってから、次のアイテムをキックします。IFlushableCollection
FlushPanel
StaggeredReleaseCollection<T>
VerticalStackFlushPanel
StaggeredReleaseCollection<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>
ですか?