A relative JsonPointerReferenceanswers/mesh-space, name, "" — means nothing on its own. It is resolved against a data context, and the context decides whether the value is read from a mesh node, from the area's /data replica, or from nowhere useful at all.

The rule

A view resolves its pointers against BlazorView.BindingDataContext, never against the raw DataContext cascading parameter.

protected string? BindingDataContext =>
    ViewModel?.DataContext is { } own && !string.IsNullOrWhiteSpace(own)
        ? WorkspaceReference.Decode(own).ToString()
        : DataContext;

The control's OWN declaration wins; the cascade is the fallback for a control that declares none.

Why the order is not a preference

DataContext is a [CascadingParameter], and its one producer in the whole product is DispatchView:

<CascadingValue Value=@ViewModelDataContext Name="DataContext">
...
ViewModelDataContext = ViewModel?.DataContext != null
    ? WorkspaceReference.Decode(ViewModel.DataContext).ToString()
    : DataContext;

That is the same expression. So for every view reached through a dispatch the two agree by construction and the order changes nothing. What it removes is the view's dependence on an out-of-band delivery of a value it already holds: the control model is a direct [Parameter], it drives the very OnParametersSetBindData pass that does the binding, and it therefore cannot be stale relative to that bind. The cascade can be absent, late, or the ancestor's.

Two views had already reached the same conclusion locally and written their own fallback — CodeEditorView and NotebookEditorView both carry GetEffectiveDataContext() => DataContext ?? ViewModel?.DataContext. That workaround is now the base class's job, for every view.

The fault this closes

MeshWeaver.Plugins#1869 / MeshWeaver#3711. Four times between 2026-09-07 and 2026-09-14 a learner's quiz answer picker failed to bind on memex-cloud, always with the same line:

fail: MeshWeaver.Blazor.EntityViews.RadioGroupView[0]
      Error binding JsonPointerReference 'answers/q1' in Area Quiz/Options1
      System.Text.Json.JsonException: 'q' is an invalid start of a value.

The token is arithmetic, and it says the data context was absent — not "a different context", not "an unparseable one". Every other value produces a different token.

Three explanations were measured and are false:

hypothesis verdict
the control leaves the server without its context QuizLayoutAreas.Choice sets DataContext = GetMeshNodeDataContext(sheetPath)
the delivered /areas/"Quiz/Options1" payload loses it on the wire ❌ measured over a real remote layout-area stream, including as a patch onto the frame it replaces
the quiz's two DispatchView hops drop it when the slot's control is swapped ❌ measured on a live component tree through the swap

What was left is the view itself: it held a control carrying the context and bound without one, because it read only the cascade. NodeBoundPointerContextTest pins all four rows, and its reproduction row fails with the production message verbatim when the fix is reverted.

And when there is genuinely no context

A relative pointer with nothing to resolve it against is left UNBOUND, with a diagnostic naming the pointer and the area. It is never handed to Stream.DataBind — because that does not refuse it either. Core's LayoutClientExtensions.GetPointer promotes a context-less relative pointer to an absolute one, and LayoutExtensions.GetStream then reads segment 0 as a collection and segment 1 as a JSON-encoded id:

pointer, no context resolves to outcome
answers/q1 /answers/q1 Deserialize<string>("q1") throws
answers /answers one segment, no id decode — binds silently against the layout stream's own root

🚨 The crash is the lucky case. The one-segment form reports nothing and reads — and through UpdatePointer, writes — a root key of the layout document, beside /areas and /data. A learner's pick landing there is worse than one that does not land: the read comes back empty either way, and the wrong write is invisible and permanent. So UpdatePointer refuses the same shape, at Error, naming the control that could not be saved.

This is also why the fix is never to make the id decode tolerant: that converts the loud case into the silent one. Core records the same rule at Doc/GUI/DataBinding"An absent DataContext does not disable the binding — it RE-ROOTS it".

The silent half, measured: the Store paywall's coupon box

The quiz is the LOUD casualty of this fault. The Store paywall is the silent one, and it is what the one-segment row of the table above costs in practice.

Store/Plugin/Source/PluginLayoutAreas.BuildCouponOptions renders

Controls.Text(new JsonPointerReference("code")) with
{
    DataContext = LayoutAreaReference.GetDataPointer("store-plugin-coupon"),
    Label = "Code", Immediate = true, ImmediateDelay = 0,
}

— a relative, ONE-SEGMENT pointer. Bound through the cascade alone and reached without it, the typed code went anywhere but /data/store-plugin-coupon/code; the coupon model kept the blank value the panel's own render seeds; the Redeem click read THAT and CouponRedemption.RedeemFree refused it with "Enter a coupon code." — while the box still showed the code, because a field sets its local value before any of this. Nothing threw, nothing was logged, and the learner was told their code was missing.

That is MeshWeaver.Plugins#1698, closed and reopened twice over three days. Two fixes were shipped against it first and neither could have held: #1635 moved the field from the ABSOLUTE /code to the relative code plus a control-level DataContext — which is what put the field on this path at all, and the issue was reopened the same evening on sets carrying it — and #1815 made a pending edit flush on focus-out, which guarantees UpdatePointer is CALLED and says nothing about where the call lands.

The reading to keep: an edit that is lost and an edit that is written somewhere else are the same state from outside, and only the second survives a fix aimed at the first. When a bound value reads back empty, ask WHERE the write went before asking whether it happened.

CouponFieldWritesThroughItsOwnContextTest (src/MeshWeaver.Blazor.Views.Test) pins it in the paywall's own shape — a control declaring its own context, a relative pointer, a real remote layout-area stream, and a Redeem click that reads the model back off the OWNER. Its TheEditLandsInTheCouponModelAndNowhereElse row asserts the entered code exists at exactly ONE position in the layout document, which is the only assertion that can tell a re-rooted write from an absent one. Measured with the fix reverted: all four rows fail with "" where the person entered WELCOME2026, and all three rows of the older FormEditReachesTheOwnerBeforeTheClickTest pass — that suite binds an ABSOLUTE /data/{id} pointer and is structurally unable to see this.

Reading a bind that came back empty

  1. Is it a DataContext question? A view that reads nothing, or the wrong thing, with no error is a context question before it is a pointer question.
  2. Look for the refusal line. Leaving '{pointer}' unbound in Area {area} means the control declares no context and none was cascaded — a layout-authoring bug, and the pointer and area in that line say which control.
  3. Only then look at the pointer. A pointer that starts with / ignores the context entirely.
Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.