0

TLDR version...

We're trying to create as basic Panel subclass with an observable Items property. The control then uses those data items to create one or more related child UI objects per data item.

Our first thought naturally was to simply subclass ItemsControl, but that doesn't seem to fit because it uses an ItemContainerGenerator which only generates one 'container' per item whereas again, we need to potentially create several (which aren't containers anyway.) Plus, all the created items have to be direct children on the panel, not held in a container which is why we can't go the route of data templates.

As such, I'm just using a standard Control and I'm trying to find the proper place/event where I should be instantiating/destroying the resulting child UI elements in response to changes in the Items collection.

Now the details...

First things first. If there was something like a ItemMultiContainerGenerator, that would be perfect, but I know of no such thing.

Ok, so simply monitor the collection for changes and put the UI generation in the CollectionChanged event! Right? That was our first guess too. The problem there is for every new 'Add' or 'Remove', we have to spin through all existing controls to 'defrag' certain indexing properties on them (think along the lines of a Grid.Row or ZIndex property) meaning if you add ten items, you run the defrag ten times, not once at the end.

Plus, that change event may come in on a different thread. If you attempted to dispatch to the main thread, your performance takes a nose-dive.

Our other attempt was to use MeasureOverride, since that was called only once in response to an InvalidateMeasure call, regardless of how many children we added or removed. The issues (there are many) with this approach is we lose context of whether something was added or removed, meaning we had to throw away all children and re-add back all new ones making this extremely inefficient. Plus, mucking around with the visual tree or setting bindings could cause the layout pass to execute multiple times since we were changing something that affects the layout, namingly the panels children.

What I'm trying to find is something that happens as part of the overall rendering process (i.e. from the time the control is told its invalid until it renders), but before the Measure/Layout passes are called. That way I can cache the adds/removes in the CollectionChanged event and simply mark the control as invalid, wait for this mystery event, then process the changes en mass, then send off the results to the layout engine and be done with it.

Using Reflector, I've tried to find out where the ItemsControl adds its children to the panel, but I didn't get too far considering the complexity of the control/ItemContainerGenerator pairing.

So where is the best place to create/add UI elements to a control based on data item changes?

4

1 回答 1

1

我认为您将不得不手动收听集合更改。有几个原因:

  • 更改您的视觉孩子将使您的布局无效。如果你改变你的孩子的尺寸或排列,你将有一个无限循环。
  • 渲染发生布局之后。通过在布局后更改子项,您要求无限循环。

希望您不必支持除 之外的任何集合ObservableCollection,因为处理一种类型的集合显然更容易。但是,我认为您绝对可以制作一个响应式控件来执行这些操作。这里有一些提示

  • 实现一个Collection支持AddRange和的自定义RemoveRange(INotifyCollectionChanged 事件支持添加或删除多个项目),因此您不必为 10 个新项目执行 10 次相同的工作。
  • 在您的集合更改处理程序中,使用 e.AddedItems 和 e.RemovedItems 而不是访问基础集合。这将防止您在枚举时收到由于集合更改而导致的异常。
  • 使用 BeginInvoke 防止在调度回 UI 线程时阻塞生产者线程
  • 为了解决您的初始化问题,ISupportInitialize如果您必须一次添加或删除多个项目,请实施并使用它来暂停您的“碎片整理”过程。当您在 XAML 中创建控件时,WPF 将自动为您添加 Begin/End 调用。
  • 如果FrameworkElement您不想要ControlTemplate. 降低开销。
  • 如果这仍然无法正常工作,因为您的基础集合更改的速度太快,并且孩子相当简单,也许您应该将它们绘制进去OnRender

我刚刚想到的另一个选择是,您可以在 上安排所有这些操作Dispatcher,这样如果发生多项更改,您只需执行一次。基本上,您会像这样存储对操作的引用:

private DispatcherOperation pendingDefragOperation;

protected void ScheduleDefrag()
{
    if (pendingDefragOperation == null)
    {
        pendingDefragOperation = Dispatcher.BeginInvoke( 
            DispatcherPriority.Render, // You may want to play around with this
            new Action(Defrag));
    }
}

你会在CollectionChanged. 在您的Defrag通话中,您将设置pendingDefragOperation为空。

于 2013-06-28T20:24:53.333 回答