gum-runtime-binding

$npx mdskill add vchelaru/Gum/gum-runtime-binding

Configure runtime bindings for Gum UI elements and Forms controls.

  • Manages data flow between view models and graphical interfaces.
  • Uses BindingContext, SetBinding, and INotifyPropertyChanged APIs.
  • Selects binding style based on target element type.
  • Updates UI properties automatically when data changes.

SKILL.md

.github/skills/gum-runtime-bindingView on GitHub ↗
---
name: gum-runtime-binding
description: Reference guide for Gum's runtime data binding system — BindingContext, SetBinding on both GraphicalUiElement visuals and FrameworkElement Forms controls, binding types (string, Binding object, lambda), and how the two systems differ.
---

# Gum Runtime Binding

## Two Binding Systems

**GraphicalUiElement** (`GumRuntime/GraphicalUiElement.Binding.cs`) — basic binding available on all visuals.
**FrameworkElement** (`MonoGameGum/Forms/Controls/`) — richer binding on Forms controls, built on top of the GUE system.

`FrameworkElement.BindingContext` delegates to its `Visual.BindingContext` — they share one context.

## BindingContext

Set on any `GraphicalUiElement` or `FrameworkElement`. Cascades automatically to all descendants unless overridden:

```csharp
root.BindingContext = viewModel;  // all children inherit it
child.BindingContext = other;     // explicit overrides inherited
```

Subscribes to `INotifyPropertyChanged` and updates bound UI properties on change.

## GraphicalUiElement Binding (Visuals)

Simple string-only binding. No converters, no modes, no path traversal:

```csharp
element.SetBinding("X", nameof(vm.Position));           // basic
element.SetBinding("Text", nameof(vm.Name), "{0:N0}");  // with format string
```

`PushValueToViewModel()` is called from property setters to write back to the VM (always two-way implicitly).

## FrameworkElement Binding (Forms)

Three binding styles, all richer than the GUE version.

### 1. String-based

```csharp
textBox.SetBinding(nameof(TextBox.Text), nameof(vm.Name));
```

Shorthand — wraps the string in a default `Binding` object internally.

### 2. Explicit Binding object

```csharp
var binding = new Binding(nameof(vm.IsEnabled))
{
    Mode = BindingMode.OneWay,
    Converter = new BoolToVisibilityConverter(),
    FallbackValue = false
};
checkBox.SetBinding(nameof(CheckBox.IsChecked), binding);
```

`Binding` properties: `Path`, `Mode` (OneWay/TwoWay/OneWayToSource), `UpdateSourceTrigger` (Default/PropertyChanged/LostFocus), `Converter`, `ConverterParameter`, `StringFormat`, `FallbackValue`, `TargetNullValue`.

### 3. Lambda / expression tree

```csharp
// Typed (preferred — compiler-checked, extracts "Child.Text" path):
textBox.SetBinding<MyVm>(nameof(TextBox.Text), vm => vm.Child.Text);

// Parameterless closure:
textBox.SetBinding(nameof(TextBox.Text), () => vm.Child.Text);
```

Extension methods in `FrameworkElementExt.cs`. `BinderHelpers.ExtractPath()` walks the expression tree to produce a dotted path string, then creates a `Binding` normally. Nested paths (e.g. `vm => vm.A.B.C`) are fully supported via `PropertyPathObserver`.

## Index-Based Binding (Forms only)

Paths support integer indexer access via `[N]` syntax. Works in string paths, Binding objects, and lambdas:

```csharp
// String path
textBox.SetBinding(nameof(TextBox.Text), new Binding("Items[0].Text"));

// Lambda
textBox.SetBinding<MyVm>(nameof(TextBox.Text), vm => vm.Items[0].Text);

// Nested: index in the middle of a path
textBox.SetBinding(nameof(TextBox.Text), new Binding("Child.Items[1].Text"));
```

All binding features work with indexed paths: modes, converters, StringFormat, FallbackValue, LostFocus trigger.

**Collection change notification:** `PropertyPathObserver` subscribes to `INotifyCollectionChanged` on collections in indexed path segments. When items are added, removed, replaced, or cleared, the binding re-evaluates. Out-of-bounds indexes resolve to null (triggering `FallbackValue` if set). Currently reacts to ALL collection changes regardless of whether the specific bound index is affected — this is intentionally broad for correctness; a future optimization could filter by index relevance.

**Limitations:** Dictionary/string key indexing is not supported.

**Implementation:** `BinderHelpers.ParseSegments()` splits paths into `PathSegment` structs (name + optional int index). `BuildGetter`/`BuildSetter` emit indexer calls via `Expression.MakeIndex` or `Expression.ArrayIndex`. `ExtractPath` handles `MethodCallExpression` (`get_Item`) and `IndexExpression` nodes from lambdas. `PropertyPathObserver` uses `GetIndexedValue()` after property resolution for indexed segments.

## Feature Comparison

| Feature | GraphicalUiElement | FrameworkElement |
|---|---|---|
| String binding | ✓ | ✓ |
| Explicit Binding object | ✗ | ✓ |
| Lambda binding | ✗ | ✓ |
| Nested paths (`A.B.C`) | ✗ | ✓ |
| Index paths (`Items[0].Text`) | ✗ | ✓ |
| Binding modes | Implicit TwoWay | Configurable |
| Converters | ✗ | ✓ |
| FallbackValue / TargetNullValue | ✗ | ✓ |
| UpdateSourceTrigger | Always PropertyChanged | Configurable |

## Key Files

| File | Purpose |
|---|---|
| `GumRuntime/GraphicalUiElement.Binding.cs` | GUE binding — BindingContext, SetBinding, PushValueToViewModel |
| `MonoGameGum/Forms/Data/Binding.cs` | Binding config class + BindingMode + UpdateSourceTrigger + IValueConverter |
| `MonoGameGum/Forms/Data/NpcBindingExpression.cs` | Forms binding engine — UpdateTarget, UpdateSource |
| `MonoGameGum/Forms/Data/PropertyPathObserver.cs` | Watches dotted paths, re-hooks on intermediate changes, weak listeners |
| `MonoGameGum/Forms/Data/BinderHelpers.cs` | Lambda path extraction, compiled getter/setter delegates |
| `MonoGameGum/Forms/Controls/FrameworkElementExt.cs` | Lambda SetBinding extension methods |
| `GumRuntime/BindableGue.cs` | Deprecated alias for GraphicalUiElement — do not use |

## Non-Obvious Behaviors

**BindableGue is deprecated.** `GraphicalUiElement` now owns all binding logic. `BindableGue` exists only as a legacy alias.

**Weak listeners in PropertyPathObserver.** Forms binding uses weak references to avoid memory leaks on deep paths. GUE binding does not — callers should unsubscribe when disposing.

**Lambda extracts path at call time, not at update time.** `vm => vm.Child.Text` becomes the static path `"Child.Text"`. If `Child` is replaced, `PropertyPathObserver` re-hooks the listener chain automatically.

**ListBox `Items` is bindable.** `listBox.SetBinding(nameof(ListBox.Items), nameof(vm.Items))` works and keeps the list in sync with an `ObservableCollection` on the VM.

More from vchelaru/Gum

SkillDescription
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-forms-itemscontrolReference 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.
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.