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