gum-forms-itemscontrol
$
npx mdskill add vchelaru/Gum/gum-forms-itemscontrolReference 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
- bump-nuget-versionBump the NuGet package versions for all 12 Gum projects (11 libraries + GumCli). Queries NuGet to check if a version exists for today, then sets the new version to YYYY.M.D.V where V increments from the latest published version today (or starts at 1). Creates a release branch named ReleaseCode_YYYY_M_D_V, commits the changes, and pushes. Run this before triggering the nuget release workflow.
- gum-cliReference guide for GumCli — the headless command-line tool for Gum projects. Load this when working on gumcli commands (new, check, codegen, codegen-init), Gum.ProjectServices, HeadlessErrorChecker, ProjectLoader, HeadlessCodeGenerationService, CodeGenerationAutoSetupService, or the FormsTemplateCreator.
- gum-docs-writingReference guide for writing Gum documentation in GitBook markdown. Load when writing or editing docs/ files, adding pages to SUMMARY.md, using GitBook hints/figures, linking between pages, or adding images.
- gum-forms-behaviorsCovers Gum's behaviors system and the design-time → runtime Forms wrapping lifecycle. Load this when working on BehaviorSave, ElementBehaviorReference, StandardFormsBehaviorNames, FormsUtilities.RegisterFromFileFormRuntimeDefaults, DefaultFromFileXxxRuntime classes, or when investigating why Forms properties cannot be set at design time in the Gum tool.
- gum-forms-controlsReference guide for Forms controls — classes inheriting from FrameworkElement. Load this when working on Button, CheckBox, ListBox, ComboBox, TextBox, ScrollViewer, or any class in Gum.Forms.Controls (or FlatRedBall.Forms.Controls). Also load when working on FrameworkElement itself, the Visual/InteractiveGue relationship, state machines, DefaultVisuals, or ReactToVisualChanged.
- gum-forms-default-visualsReference guide for Forms DefaultVisuals — the code-only visual classes that back Forms controls. Load when working on ButtonVisual, any *Visual class in DefaultVisuals/, Styling, DefaultFormsTemplates registration, or building custom code-only Forms visuals.
- gum-layout>
- gum-layout-engine>
- gum-localizationReference guide for Gum's runtime localization system — ILocalizationService, CSV/RESX loading, Text vs TextNoTranslate paths, Forms control localization patterns, and gotchas.
- gum-property-assignmentReference guide for how Gum applies variables and sets properties on renderables. Load this when working on ApplyState, SetProperty, SetVariablesRecursively, CustomSetPropertyOnRenderable, font loading, IsAllLayoutSuspended, or isFontDirty.