A relative JsonPointerReference — answers/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 OnParametersSet → BindData 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
- Is it a
DataContextquestion? A view that reads nothing, or the wrong thing, with no error is a context question before it is a pointer question. - 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. - Only then look at the pointer. A pointer that starts with
/ignores the context entirely.