NE.Standard

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:

  1. 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)
    

    UIValidationTrigger is Change (default), Blur, or Submit. UIValidationSeverity is Error (default — stops a submit and draws the required marker when it is a Required rule), Warning, or Info (shown under the field, never colours it).

  2. The controller, through the bindable Validation property every IInputComponent has:

    new TextInputComponent().BindValidation(nameof(FormController.NameValidation))
    
    public static UIValidationMessage Error(string message)
    public static UIValidationMessage Warning(string message)
    public static UIValidationMessage Info(string message)
    

    null means 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.

  3. 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.