我正在编写一个自定义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
知道如何处理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显示与绑定StaggeredReleaseCollection<T>
不同的顺序?
答案 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阶段动画的完成并重新安排下一个项目。
这是我想要探索的东西。