Class ItemsControl
Generates one ItemContainer per item in ItemsSource, via ItemTemplate/ItemTemplateSelector, and virtualizes them - only the containers intersecting the current viewport (plus a small scroll-ahead buffer) are ever realized.
public class ItemsControl : Control, INotifyPropertyChanging, IBindingTarget, INotifyPropertyChanged, INotifyFocusChanged, IContainerLayout, IVirtualizingScrollInfo
- Inheritance
-
ItemsControl
- Implements
- Derived
- Inherited Members
- Extension Methods
Remarks
Implements IVirtualizingScrollInfo directly so a hosting ScrollViewer can
delegate extent/offset/viewport handling to it instead of fully measuring/arranging every item - see the
Phase 2 design spec (docs/superpowers/specs/2026-09-04-itemscontrol-design.md) for the full
virtualization/estimation design this class implements.
Carries no selection state - see ItemContainer's own remarks for why, and where selection is expected to be added later.
Realization happens inside OnViewportChanged(float, float, float, float). A hosting ScrollViewer (or any other
host implementing the IVirtualizingScrollInfo delegation pattern) calls it whenever the offset or
viewport size changes. Without such a host - directly inside a Grid or StackPanel, say - the
control reports its own content area as the viewport after each arrange (see RealizesWithoutScrollHost),
so it shows the items that fit, still virtualized, without scrolling.
Fields
ScrollAheadBuffer
Scroll-ahead buffer, in pixels, added past the visible viewport before an item that's scrolled out is de-realized - shared by this class's own RealizeRange(int, float) and any subclass that overrides it with different geometry (e.g. a uniform grid), so the same forward-scroll headroom applies everywhere without needing to be tuned in more than one place.
protected const float ScrollAheadBuffer = 100
Field Value
Properties
DefaultEstimatedItemHeight
Gets or sets the estimated height given to an item that hasn't been realized/measured yet - used only before anything in ItemsSource has ever been realized; once at least one item has a real measured height, the running average of known heights is used instead.
[Range(0, Infinity, MinimumIsExclusive = true)]
public float DefaultEstimatedItemHeight { get; set; }
Property Value
Remarks
Must be greater than 0.
Exceptions
- ArgumentOutOfRangeException
The value being set is not greater than zero.
ExtentHeight
Gets this content's full natural height, estimated or exact depending on how much of it has actually been measured so far.
public float ExtentHeight { get; }
Property Value
ExtentWidth
Gets this content's full natural width, estimated or exact depending on how much of it has actually been measured so far.
public float ExtentWidth { get; }
Property Value
HorizontalOffset
Gets or sets the horizontal scroll offset, in pixels, most recently reported via OnViewportChanged(float, float, float, float).
protected float HorizontalOffset { get; set; }
Property Value
ItemCount
Gets the number of items currently in ItemsSource.
protected int ItemCount { get; }
Property Value
ItemTemplate
Gets or sets the template used to build each item's visual tree, when ItemTemplateSelector doesn't resolve one for a given item.
public DataTemplate? ItemTemplate { get; set; }
Property Value
Remarks
When neither this nor ItemTemplateSelector provides a template, each item is shown as a TextBlock with its ToString() text.
ItemTemplateSelector
Gets or sets a delegate that picks a DataTemplate per item, overriding ItemTemplate for items it returns one for.
public Func<object, DataTemplate>? ItemTemplateSelector { get; set; }
Property Value
ItemsSource
Gets or sets the collection this control generates one ItemContainer per item from.
public IEnumerable? ItemsSource { get; set; }
Property Value
Remarks
When the assigned value implements INotifyCollectionChanged, this control stays live-reactive to it for as long as it remains assigned (see OnSourceCollectionChanged(object, NotifyCollectionChangedEventArgs)). A plain IEnumerable is enumerated once, into a private snapshot - later external mutation of that source is not observed.
PoolingEnabled
Gets or sets a value indicating whether de-realized ItemContainers are pooled for reuse by a later item that resolves the same DataTemplate, instead of being discarded.
public bool PoolingEnabled { get; set; }
Property Value
RealizationBounds
Gets the rectangle realized containers are positioned within - ContentBounds by default. A subclass that displays realized containers somewhere other than its own content area (e.g. a popup) overrides this to redirect positioning, without changing the realize/de-realize/virtualization algorithm itself.
protected virtual Rectangle RealizationBounds { get; }
Property Value
RealizedContainers
Gets the containers currently realized, by item index.
protected Dictionary<int, ItemContainer> RealizedContainers { get; }
Property Value
Remarks
Derived controls that lay out their own containers (TabControl, PropertyGrid, ...) read and index it; realize and de-realize containers through the base methods rather than editing it directly. It's the concrete type, so enumerating it in a layout pass doesn't allocate.
RealizesWithoutScrollHost
Gets a value indicating whether this control realizes items within its own content area when no scroll host reports a viewport. true by default.
protected virtual bool RealizesWithoutScrollHost { get; }
Property Value
Remarks
A control that realizes its items somewhere else (a Selector realizes them into its popup) overrides this to return false.
VerticalOffset
Gets or sets the vertical scroll offset, in pixels, most recently reported via OnViewportChanged(float, float, float, float).
protected float VerticalOffset { get; set; }
Property Value
ViewportHeight
Gets or sets the visible viewport height, in pixels, most recently reported via OnViewportChanged(float, float, float, float).
protected float ViewportHeight { get; set; }
Property Value
ViewportWidth
Gets or sets the visible viewport width, in pixels, most recently reported via OnViewportChanged(float, float, float, float).
protected float ViewportWidth { get; set; }
Property Value
Methods
ArrangeContent()
Handles custom arrange logic when overridden in any UIElement instance.
protected override void ArrangeContent()
Remarks
This method is called during the arrange pass to position and size the element's content within its final bounds. The element's position and size are already determined by the layout system and available in the ActualBounds property.
When implementing this method:
- Position child elements within the available space;
- Respect the element's padding when positioning content;
- Handle any content-specific layout requirements;
- Call Arrange() on child elements if needed.
AttachContainer(ItemContainer, int)
Wires a freshly realized container into the visual tree - Parent/
Canvas point directly at this by default. A subclass that hosts
realized containers elsewhere (e.g. a popup panel) overrides this to redirect them there instead.
protected virtual void AttachContainer(ItemContainer container, int index)
Parameters
containerItemContainerThe freshly realized container.
indexintThe item index
containerwas realized for.
ComputeExtentHeight()
Computes ExtentHeight - the running-average single-column estimate by default. A subclass with different virtualization geometry (e.g. a uniform grid) overrides this with its own formula instead.
protected virtual float ComputeExtentHeight()
Returns
- float
The total height of all items in the collection, estimated via a running average of known heights.
CreateContainer(DataTemplate, object)
Builds a fresh ItemContainer to host item's built visual tree - the
default body RentContainer(DataTemplate, object) falls back to whenever pooling can't supply
one. A Selector overrides this to realize SelectorItems instead.
protected virtual ItemContainer CreateContainer(DataTemplate template, object item)
Parameters
templateDataTemplateThe template to build
item's content with.itemobjectThe data item the new container is being realized for.
Returns
- ItemContainer
The freshly built container.
Derealize(int)
De-realizes index if it's currently realized - remeasures it one last time (folding
any final size change into the height cache, see the Phase 2 design spec §6), detaches it, and returns
it to its template's pool when PoolingEnabled.
protected void Derealize(int index)
Parameters
indexintThe index of the item to derealize.
DerealizeOutOfRange(Func<int, bool>)
De-realizes every currently realized index for which isStillInRange returns
false - the shared trailing cleanup step of RealizeRange(int, float),
factored out so a subclass with different virtualization geometry (e.g. a uniform grid) can reuse the
same de-realize sweep instead of re-implementing it.
protected void DerealizeOutOfRange(Func<int, bool> isStillInRange)
Parameters
DetachContainer(ItemContainer)
Detaches container from wherever AttachContainer(ItemContainer, int) wired
it - the exact inverse.
protected virtual void DetachContainer(ItemContainer container)
Parameters
containerItemContainerThe container being de-realized.
EnsureRealized(int)
Realizes index if it isn't already, wiring it into the visual tree, measuring it,
and folding its real height into the height cache (see RecordHeight(int, float)).
protected void EnsureRealized(int index)
Parameters
indexintThe index of the item to ensure is realized.
GetItemAt(int)
Gets the item at index in ItemsSource.
protected object GetItemAt(int index)
Parameters
Returns
- object
The item.
GetItemExtent(int)
Gets where the item at index sits in this control's scrollable content - walked from the
current viewport anchor through measured heights and the running estimate, the same geometry
RealizeRange(int, float) positions containers with. A subclass with different geometry (e.g. a
uniform grid) overrides this.
protected virtual (float Top, float Height) GetItemExtent(int index)
Parameters
indexintThe item's index.
Returns
GetVisualChildren()
Enumerates this element's immediate visual children, in paint order (back-to-front), for hit-testing and tree traversal.
protected override IEnumerable<UIElement> GetVisualChildren()
Returns
- IEnumerable<UIElement>
The element's immediate children. The base implementation yields none (a leaf element).
Remarks
Container elements (e.g. Panel, Border) override this to expose their children, in the exact order they're drawn in — HitTest(Vector2) depends on this matching actual paint order, or clicks will target the wrong, visually-obscured element.
IndexOfItem(object?)
Gets the index of item within ItemsSource.
protected int IndexOfItem(object? item)
Parameters
itemobjectThe item to search for.
Returns
- int
The zero-based index of
item, or-1if it isn't in the collection (including whenitemis null).
LocateViewportStart()
Finds the item whose slot contains the current verticalOffset - via a short walk from the last
anchor for a small scroll delta, or a direct estimate for a big jump (spec §5).
A subclass with different virtualization geometry overrides this to replace the algorithm entirely.
protected virtual (int Index, float Offset) LocateViewportStart()
Returns
- (int Index, float Offset)
A tuple containing the index of the first item to realize and the pixel offset of its top edge.
MeasureContent()
Handles custom measure logic when overridden in any UIElement instance.
protected override Size MeasureContent()
Returns
Remarks
This method is called during the measure pass to determine the desired size of the element. The returned size should represent the natural size of the element's content, without considering any constraints, margins, or padding.
When implementing this method:
- Return the natural size needed to display the content;
- Do not include padding, margins, or borders in the returned size;
- Consider the content's actual dimensions (e.g., text length, image size);
- Return Empty if the element has no content.
OnAttached()
Raises the Attached event.
protected override void OnAttached()
Remarks
Propagates Canvas to every element in GetVisualChildren() - this is what makes attaching a whole pre-built subtree (the common pattern: build children, then attach the root once via Add(UIElement)) reach every descendant, not just the immediate root. Each child's own Canvas setter recurses into its children the same way, so this walks the full subtree regardless of depth. Container-specific child-add paths (e.g. OnChildAdded(object?, GenericEventArgs<UIElement>)) still assign Canvas directly for children added after the parent is already attached - this cascade is what covers everything built beforehand.
OnDetached()
Raises the Detached event.
protected override void OnDetached()
Remarks
Propagates null to every element in GetVisualChildren(), mirroring OnAttached() - see its remarks.
OnItemsChanged()
Called after items has been mutated by any means - a full ItemsSource reassignment
(Icy.UI.Controls.ItemsControl.ResetItems()), a live incremental collection-change notification, or a full
Reset re-snapshot. Empty by default. A subclass with state
that depends on item identity/count (e.g. SelectingItemsControl's
SelectedIndex) overrides this to keep it valid.
protected virtual void OnItemsChanged()
OnRender(IRenderContext)
Handles custom render logic when overridden in any UIElement instance.
protected override void OnRender(IRenderContext context)
Parameters
contextIRenderContextThe context to render with.
OnViewportChanged(float, float, float, float)
Called by the host ScrollViewer whenever the offset or viewport size it's presenting this content through may have changed - the single point where this content decides what to realize, de-realize, and how to position it.
public virtual void OnViewportChanged(float newHorizontalOffset, float newVerticalOffset, float newViewportWidth, float newViewportHeight)
Parameters
RealizeRange(int, float)
Realizes every item whose slot overlaps the viewport (plus a forward-only scroll-ahead buffer),
starting the walk at firstIndex/firstOffset; positions each
realized container, and de-realizes anything realized but no longer in range.
A subclass with different virtualization geometry overrides this to replace the algorithm entirely.
protected virtual void RealizeRange(int firstIndex, float firstOffset)
Parameters
firstIndexintThe index of the first item to realize in this range walk.
firstOffsetfloatThe pixel offset of the top edge of the first item to realize.
RecordRealizedHeight(int, float)
Folds a freshly measured (EnsureRealized(int)) or finally-measured (Derealize(int)) item's height into the height cache via RecordHeight(int, float) by default. A subclass whose virtualization geometry doesn't depend on measured content height (e.g. a uniform grid, where every cell uses a fixed, known-upfront size) overrides this to no-op - honoring it there would misapply the single-column height-cache's above-viewport anchor correction (see RecordHeight(int, float)) to geometry it has no bearing on.
protected virtual void RecordRealizedHeight(int index, float height)
Parameters
indexintThe item index being realized or de-realized.
heightfloatThe container's just-measured height.
ScrollIntoView(int)
Scrolls the hosting ScrollViewer just enough to show the item at index entirely.
public void ScrollIntoView(int index)
Parameters
indexintThe index of the item to show, within
[0, item count).
Remarks
- An item above the viewport is aligned with its top edge, and one below with its bottom edge.
- An item that is already fully visible doesn't scroll anything.
- Positions come from measured heights where they're known and from the running estimate elsewhere. Once the item is realized, the usual height correction (VerticalOffsetCorrectionRequested) settles any difference.
- Without a host that has laid this control out, nothing happens.
Exceptions
- ArgumentOutOfRangeException
indexis outside the item range.
Events
ScrollToVerticalOffsetRequested
Occurs when this content asks its host ScrollViewer to scroll to an absolute vertical offset - for example to bring one of its items into view (see ScrollIntoView(int)).
public event EventHandler<float>? ScrollToVerticalOffsetRequested
Event Type
Remarks
The event's float payload is the new VerticalOffset, not a delta. The host clamps it to its scrollable range.
VerticalOffsetCorrectionRequested
Occurs when this content needs its host ScrollViewer to adjust VerticalOffset by a specific amount, so on-screen content doesn't visibly jump when something already realized (and positioned above the current viewport) changes size.
public event EventHandler<float>? VerticalOffsetCorrectionRequested
Event Type
Remarks
The event's float payload is the delta to apply
(VerticalOffset += delta), not an absolute new value.