Binding
A binding ties a component property to a location in the controller's object graph, addressed by a
RecursivePath. This page covers how to write a path, which method binds it, and what happens when the
value it resolves to is missing or invalid.
Paths
A RecursivePath is an immutable list of segments: a property name, an index ([0]), or a key
(["abc"] or the bare [abc], both parsed the same way when the token is not an integer). ToString()
renders the grammar and RecursivePath.Parse(path) reads it back:
Groups[0].SubItems[2].Title
Every Bind* method that takes a string parses it with RecursivePath.Parse — use nameof for a plain
property so a rename does not silently break the binding:
new TextComponent().BindTitle(nameof(CounterController.Count))
Bind* vs the raw Bind
A bindable property generates its own Bind<Name>(string path, UIBindingScope scope = ..., UIBindingMode mode = ...) — the defaults come from how the property was declared (see Modes, below). There is also a raw
entry point every component inherits:
public TComponent Bind(UIProperty property, string path, UIBindingScope scope = UIBindingScope.Root, UIBindingMode mode = UIBindingMode.OneWay)
Reach for the raw Bind only when binding by property key at runtime (a template that binds by
IVisualComponent property object rather than by name) — its mode always defaults to OneWay,
regardless of what the property itself declares as its default. A template that needs TwoWay has to pass
UIBindingMode.TwoWay explicitly; a missed one binds one-way with no error.
Bind throws at view-compile time if the property is not registered for the component, is not bindable
(IsBindable = false), or does not support the requested mode.
Scopes
UIBindingScope says where a path is resolved from:
| Value | Resolves from |
|---|---|
Relative |
The component's own context — inside an item template, the row being rendered. Pass it explicitly there; nothing defaults to it. |
Parent |
One level up from the context this component belongs to — not one visual hop. A plain visual child only inherits its context, so Parent on a component with no context of its own lands one level above its nearest context-defining ancestor. |
Root |
The controller root. This is what a raw Bind and every generated Bind* default to — inside an item template this is almost never what is meant. |
A nested items example: an items view binds its rows from the controller, and its row template — one component tree, compiled once and reused for every row — binds relative to whichever row it is currently rendering:
new ItemsViewComponent()
.BindItems(nameof(ContextMenuGroupContext.Deploys), UIBindingScope.Relative)
.SetTemplate(new ActionComponent()
.BindTitle(nameof(DemoDeploymentRow.Service), UIBindingScope.Relative)
)
Inside a template a plain visual child inherits its enclosing row's context, so Relative there resolves
against the row rather than the view root — the source of the "almost never Root" rule above. Nesting one
more items view inside a row template compounds the same rule: its own Relative binding appends to the
row's path, producing something like "Groups[].SubItems" for the inner collection.
Modes
UIBindingMode:
| Value | Direction |
|---|---|
OneWay |
Source to target only. |
TwoWay |
Both directions. |
OneWayToSource |
Target to source only — the initial change set does not carry this value, so nothing but the renderer ever writes it; use it for state the server only stores. |
OnSubmit |
Target-to-source updates are buffered on the client until an explicit submit (ButtonComponent.OnSubmit(formId, command)), then flushed before the command runs. Unlike OneWayToSource, the initial change set does carry this value. |
A property must declare the matching BindingCapabilities (UIBindingCapabilities: SourceToTarget,
TargetToSource, SubmitBufferedTargetToSource, a [Flags] enum) for a mode to be accepted —
IInputComponent.Value declares all three plus DefaultBindingMode = UIBindingMode.TwoWay, which is why
BindValue defaults to two-way without saying so.
What a null value renders
A binding that resolves to null does not clear the property — it falls back to the value the
component was authored with (Set*), or, absent that, the property's registered default. A controller
holding null for a bound property therefore renders exactly what the same component renders unbound. This
is also why a property both Set* and Bind*-ed comes back to the set value rather than to nothing when
the bound value is missing.
Static-only properties
A property declared IsBindable = false, GenerateBinder = false generates no Bind<Name> method, and
Bind(UIProperty, …) refuses it at compile time with a thrown error — never a silent no-op. Reach for this
when a property is genuinely render-time-only (read once to build a picker, for instance); it is not a
workaround for "I don't want this bound yet."
Responsive values
Any UIResponsive<T>? property gets a convenience Set* overload alongside its plain one:
public TComponent SetWidth(UILayoutLength value, UILayoutLength? sm = null, UILayoutLength? md = null, UILayoutLength? xl = null, UILayoutLength? xxl = null)
sm, md, xl, xxl are the breakpoint tiers; an unset tier falls back to the next narrower one that is
set, down to the base value (mobile-first). SetPlacement on every visual component follows the same shape
(see views, placement grid).
BindContext
BindContext(path, scope) binds the component's context rather than one of its properties — the base
path every relative binding and action argument inside this component (and its descendants, if it is a
container) resolves against:
public virtual TComponent BindContext(string path, UIBindingScope scope = UIBindingScope.Root)
public virtual TComponent BindContext(RecursivePath path, UIBindingScope scope = UIBindingScope.Root)
Two-way sync
A two-way-bound value syncs on the native change event, not input — fewer round-trips. The property
still needs its capability declared correctly (see Modes, above) or the client's write is silently dropped
by the runtime rather than applied.
Validation on inputs
An input's validation message comes from up to three sources, and the strongest one shown wins:
Client rules, added fluently:
new TextInputComponent() .Required("Name is required.") .Regex(@"^\S+@\S+$", "Enter a valid email.", trigger: UIValidationTrigger.Blur, severity: UIValidationSeverity.Warning)public static TComponent Required<TComponent>(this TComponent input, string message, UIValidationTrigger trigger = UIValidationTrigger.Change, UIValidationSeverity severity = UIValidationSeverity.Error) public static TComponent Regex<TComponent>(this TComponent input, string pattern, string message, UIValidationTrigger trigger = UIValidationTrigger.Change, UIValidationSeverity severity = UIValidationSeverity.Error) public static TComponent Validate<TComponent>(this TComponent input, UIValidationTrigger trigger, UIComparisonOperator @operator, object? value, string message, UIValidationSeverity severity = UIValidationSeverity.Error)UIValidationTriggerisChange(default),Blur, orSubmit.UIValidationSeverityisError(default — stops a submit and draws the required marker when it is aRequiredrule),Warning, orInfo(shown under the field, never colours it).The controller, through the bindable
Validationproperty everyIInputComponenthas:new TextInputComponent().BindValidation(nameof(FormController.NameValidation))public static UIValidationMessage Error(string message) public static UIValidationMessage Warning(string message) public static UIValidationMessage Info(string message)nullmeans no message. A controller's own message never blocks a submit — the controller wrote it and judges the value again on the next attempt; clearing it is the controller's job.The runtime, when it refuses a value the controller could not take.
Only an error from a client rule or the runtime stops OnSubmit(formId, command) from dispatching — the
controller's own error does not gate the press.
Where to go next
- Views for the placement grid and the fluent style
Bind*is part of. - Controllers for the paths
[RecursiveMember]properties expose. - Interactions and effects for interactions, which write a property directly rather than through a binding.