gum-forms-itemscontrol

$npx mdskill add vchelaru/Gum/gum-forms-itemscontrol

Reference ItemsControl and ListBox mechanics for managing visual list synchronization and item templates.

  • Resolves issues related to list item desynchronization and collection binding behavior.
  • Provides context for ItemsControl, ListBox, and associated template classes.
  • Guides development when manipulating list contents or visual representations.
  • Offers detailed documentation on the relationship between data and visual elements.
SKILL.md
.github/skills/gum-forms-itemscontrolView on GitHub ↗
---
name: gum-forms-itemscontrol
description: Reference guide for ItemsControl and ListBox — the Items/ListBoxItems relationship, templates, InnerPanel sync, and gotchas. Load this when working on ItemsControl, ListBox, ListBoxItem, VisualTemplate, FrameworkElementTemplate, Items collection behavior, ListBoxItems desync, or adding/removing items from a list box.
---

# ItemsControl and ListBox Reference

## Key Files

| File | Purpose |
|------|---------|
| `MonoGameGum/Forms/Controls/ItemsControl.cs` | Base class: Items property, template resolution, InnerPanel sync |
| `MonoGameGum/Forms/Controls/ListBox.cs` | Adds ListBoxItems tracking, selection, and ListBoxItem creation |
| `MonoGameGum/Forms/Controls/ListBoxItem.cs` | Individual row control; holds IsSelected, IsHighlighted, events |
| `MonoGameGum/Forms/VisualTemplate.cs` | Creates `GraphicalUiElement` instances (visual-first) |
| `MonoGameGum/Forms/FrameworkElementTemplate.cs` | Creates `FrameworkElement` instances (forms-first) |

## The Two Collections

`Items` and `ListBoxItems` are separate and can get out of sync.

- **`Items`** (`IList`, default `ObservableCollection<object>`) — the logical data collection. Can hold anything: strings, view models, `ListBoxItem` instances, or any `FrameworkElement` / `GraphicalUiElement`.
- **`ListBoxItems`** (`ReadOnlyCollection<ListBoxItem>`) — the visual row controls actually shown. Wraps `ListBoxItemsInternal` (a `List<ListBoxItem>`).

In normal usage (adding data objects to `Items`) they stay in sync. They diverge in several cases — see **Desync Gotchas** below.

## Data Flow: Items → InnerPanel → ListBoxItems

Adding to `Items` triggers a two-stage pipeline:

1. **`HandleItemsCollectionChanged`** — responds to `Items`. Creates or locates a visual and inserts it into `InnerPanel.Children`.
2. **`HandleInnerPanelCollectionChanged`** — responds to `InnerPanel.Children`. Calls `HandleCollectionNewItemCreated(frameworkElement, index)`.
3. **`HandleCollectionNewItemCreated`** (ListBox override) — if the item is a `ListBoxItem`, inserts it into `ListBoxItemsInternal` and calls `AssignListBoxEvents`.

`HandleCollectionNewItemCreated` is NOT called directly from step 1. It is only triggered by InnerPanel firing its own `CollectionChanged`. This indirection is intentional.

## What Gets Created Per Item Type

`HandleItemsCollectionChanged` dispatches based on what was added to `Items`:

| Item type added to `Items` | What happens |
|---------------------------|-------------|
| `FrameworkElement` | Its `.Visual` is inserted into InnerPanel directly — no new wrapper created |
| `GraphicalUiElement` | Inserted into InnerPanel directly — no wrapper |
| Any other data object AND `VisualTemplate` is set | `VisualTemplate.CreateContent(item, createFormsInternally:false)` is called; result inserted |
| Any other data object, no `VisualTemplate` | `CreateNewItemFrameworkElement(item)` is called |

**ListBox overrides `CreateNewItemFrameworkElement`** with additional logic:

| Item type | ListBox behavior |
|-----------|----------------|
| `ListBoxItem` | Used as-is — no template, no wrapping |
| Anything else | Calls `CreateNewVisual(vm)` (uses `VisualTemplate` or `DefaultFormsTemplates[typeof(ListBoxItem)]`), then wraps result in a `ListBoxItem` via `CreateNewListBoxItem`. `BindingContext` and `UpdateToObject` are called on the result. |

## Templates

There are two template types with different roles:

**`VisualTemplate`** — produces a `GraphicalUiElement` (visual-first).
- Constructed with a `Type` (must have `(bool, bool)` or no-arg constructor), `Func<GraphicalUiElement>`, `Func<object, GraphicalUiElement>`, or `Func<object, bool, GraphicalUiElement>`.
- Used by `CreateNewVisual`. When constructed from a `Type`, calls it with `(true, false)` — `createFormsInternally:false` prevents the visual from creating its own Forms object, since the ListBox will wrap it.
- Set on `ItemsControl.VisualTemplate`. Changing it clears and rebuilds all visuals.

**`FrameworkElementTemplate`** — produces a `FrameworkElement` (forms-first).
- Constructed with a `Type` or `Func<FrameworkElement>`.
- Used by `CreateNewItemFrameworkElement`. For ListBox, the result must be a `ListBoxItem` subclass or an exception is thrown.
- Set on `ItemsControl.FrameworkElementTemplate`.

**Global fallback** — if neither template is set, `DefaultFormsTemplates[typeof(ListBoxItem)]` is used (set during app initialization). This is the normal path for default apps.

**Setting a template clears and rebuilds all existing items** — both `VisualTemplate` and `FrameworkElementTemplate` setters call `ClearVisualsInternal()` and replay the Items collection.

## Desync Gotchas

### 1. Adding a non-ListBoxItem FrameworkElement to Items

When a `Button`, `CheckBox`, or other `FrameworkElement` is added to `Items`, its Visual is inserted into InnerPanel. `HandleInnerPanelCollectionChanged` fires, but `asGue.FormsControlAsObject` is the `Button` (not a `ListBoxItem`), so `HandleCollectionNewItemCreated` is called with a `Button`. ListBox's override only inserts into `ListBoxItemsInternal` if the item `is ListBoxItem`, so the `Button` is silently skipped. **`Items.Count` increases but `ListBoxItems.Count` does not.**

### 2. Adding directly to InnerPanel.Children

`HandleInnerPanelCollectionChanged` fires and can populate `ListBoxItemsInternal` if the child's `FormsControlAsObject` is a `ListBoxItem`. But **`Items` is never updated** — it stays at 0 (or whatever it was). This is the case when a ListBox's visual is constructed by the Gum tool with pre-filled children.

### 3. Gum tool pre-filled ListBox (ReactToVisualChanged recovery)

If a ListBox Visual arrives with children already in InnerPanel and `Items.Count == 0`, `ReactToVisualChanged` in ListBox iterates `InnerPanel.Children`, adds `ListBoxItem` instances to both `Items` and `ListBoxItemsInternal`, and calls `AssignListBoxEvents`. This recovery only runs once at construction; it does not stay in sync afterward.

### 4. Index alignment assumption

`HandleItemSelected` resolves the data object via `Items[ListBoxItemsInternal.IndexOf(listBoxItem)]`. If `Items` and `ListBoxItems` have drifted (any of the above cases), **selection silently fails or selects the wrong item** — the check `clickedIndex >= Items.Count` causes an early return.

### 5. AssignListBoxEvents idempotency

`ListBoxItem.AssignListBoxEvents` is guarded by `hasHadListBoxEventsAssigned`. A `ListBoxItem` that bypasses normal item creation (e.g., added to InnerPanel directly without going through `Items`) may or may not have its events assigned. If events are missing, the item renders but clicking it produces no selection change.

## Selection

`SelectedItems` is an `ObservableCollection<object>` (not replaceable, only modified). `SelectedObject` and `SelectedIndex` are convenience properties that read/write the first entry in `SelectedItems`.

`SyncIsSelectedFromSelectedItems` walks `ListBoxItemsInternal` and reconciles `IsSelected` on each item. It runs whenever `SelectedItems` changes or `SelectedObject`/`SelectedIndex` are set.

`SelectionMode` controls click behavior: `Single` (default), `Multiple` (each click toggles), `Extended` (Ctrl/Shift modifier keys). Gamepad/keyboard input always uses single-selection behavior regardless of mode.

## DisplayMemberPath

When set, `DisplayMemberPath` causes `listBoxItem.UpdateToObject(property_value_as_string)` instead of `UpdateToObject(the_object_itself)`. This applies both on initial creation and when `DisplayMemberPath` is changed after items are already loaded.
More from vchelaru/Gum