我正在编写一个自定义 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
知道如何处理IFlushableCollection
和FlushPanel
(它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>
?