Table of Contents

Class ItemsControl

Namespace
Icy.UI.Controls
Assembly
IcyUI.dll

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

float

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

float

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

float

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

float

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

float

ItemCount

Gets the number of items currently in ItemsSource.

protected int ItemCount { get; }

Property Value

int

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

DataTemplate

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

Func<object, DataTemplate>

ItemsSource

Gets or sets the collection this control generates one ItemContainer per item from.

public IEnumerable? ItemsSource { get; set; }

Property Value

IEnumerable

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

bool

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

Rectangle

RealizedContainers

Gets the containers currently realized, by item index.

protected Dictionary<int, ItemContainer> RealizedContainers { get; }

Property Value

Dictionary<int, ItemContainer>

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

bool

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

float

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

float

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

float

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

container ItemContainer

The freshly realized container.

index int

The item index container was 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

template DataTemplate

The template to build item's content with.

item object

The 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

index int

The 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

isStillInRange Func<int, bool>

Reports whether a currently realized index is still within range.

DetachContainer(ItemContainer)

Detaches container from wherever AttachContainer(ItemContainer, int) wired it - the exact inverse.

protected virtual void DetachContainer(ItemContainer container)

Parameters

container ItemContainer

The 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

index int

The index of the item to ensure is realized.

GetItemAt(int)

Gets the item at index in ItemsSource.

protected object GetItemAt(int index)

Parameters

index int

A zero-based index within [0, ItemCount).

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

index int

The item's index.

Returns

(float Top, float Height)

The item's top offset and its height.

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

item object

The item to search for.

Returns

int

The zero-based index of item, or -1 if it isn't in the collection (including when item is 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

Size

Size recommended to display the UIElement.

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

context IRenderContext

The 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

newHorizontalOffset float
newVerticalOffset float
newViewportWidth float
newViewportHeight float

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

firstIndex int

The index of the first item to realize in this range walk.

firstOffset float

The 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

index int

The item index being realized or de-realized.

height float

The 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

index int

The 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

index is 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

EventHandler<float>

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

EventHandler<float>

Remarks

The event's float payload is the delta to apply (VerticalOffset += delta), not an absolute new value.