You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
25 of 72 control views silently drop Style/Class — the API lets you set what the view ignores #1297
Style and Class are public API on UiControl (src/MeshWeaver.Layout/UiControl.cs:16,52), and WithStyle(...) is used widely — but 25 of 72 control views never apply them. An author sets a style, the view drops it, and nothing reports anything.
Why this is filed rather than fixed
PR #1288 fixed HtmlView, the worst instance: Controls.Html(x).WithStyle("width:100%") was silently a no-op, which is why three Reinsurance charts rendered at ~6% of column width. The audit below came out of that work and is deliberately not included in it — a 25-view sweep needs its own review.
The audit (72 control views under src/MeshWeaver.Blazor/)
Bucket
Count
🔴 Silently drops — 17 drop both, 8 drop Class only
25
🟡 Unclear (NamedAreaView)
1
🟢 Applies both
39
⚪ Deliberately N/A (no meaningful box)
7
Plus three outside the buckets: SearchBoxView, MeshSearchView, Monaco/CodeEditorView are registered as control views in BlazorViewRegistry but derive from plain ComponentBase, so Style/Class are never bound at all.
Standouts:
The 8 Class-only drops look like plain omissions rather than design — they bind Style and forget Class.
MeshNodePickerView is a FormComponentBase and drops Width/Height too.
What "fixing" should and should not mean
🚨 Not a blanket wrapper.#1288 established why, with a case that would have shipped undetected: samples/Graph/Data/ACME/Project/Todo/Source/TodoLayoutAreas.cs:117 sets flex: 1 on its fragment's own root because that root is the direct flex child. A wrapper makes it inert — and that file is in-mesh source, so no build and no test would catch it. Each of the 25 needs its own judgement about where the style belongs.
Also from #1288, worth reusing: gate on the declaration (ViewModel.Style), not the resolved value. Either can be a JsonPointerReference that lands a frame later, so gating on the value renders bare and then restructures the DOM mid-flight.
The general defect, which outlives any individual view
The API lets an author set something the view may silently ignore, and there is no signal — no log, no failure, no test. That is the same shape as the other silent-success traps collected today (a gate passing on a stale tag set, dotnet test --no-restore on an unbuilt project exiting 0). Worth considering whether a view that cannot honour a declared Style should say so, so the 26th instance reports itself instead of being found by someone squinting at a 6%-wide chart.
Full per-view classification is in PR #1288's body.
Swept, split into three stacked PRs so each stays reviewable. All 25 views fixed, plus the
"unclear" one resolved and the three out-of-bucket ones tracked separately.
Merge order is #1323 → #1327 → #1332 — each is based on the previous one because they share the ClassSuffix / StyleSuffix helpers on BlazorView. GitHub re-targets each to main automatically as
its base merges.
No blanket wrapper — and no new element anywhere
Not one of the 25 gained a wrapper. Every fix lands on an element the view already renders, chosen
per view; in the two cases where the view has no element of its own the declaration is forwarded to
the control it delegates to (MeshNodeCardView's embedded area), the shape PropertyView / EditFormView already use. So the TodoLayoutAreas.cs:117 class of regression — flex: 1 on a
fragment's own root, in in-mesh source no build type-checks — cannot arise from this sweep.
Two rules came out of it that were not in the original audit:
The author's Style goes LAST in an attribute that already has literals, so a declared max-width overrides the view's default. Dropping the declaration was the bug; letting the default win would be
the same bug with extra steps. Asserted as ordering, not presence.
Gating on the declaration (ViewModel.Style) is specific to the wrapper case and was deliberately not copied here: no element is created or removed, so a late-resolving JsonPointerReference just
updates an attribute — it cannot restructure the DOM mid-flight. Applying fix: HtmlView honours Style/Class instead of silently dropping them #1288's rule mechanically
would have been cargo-culting.
Backwards compatibility
Both helpers return the empty string when nothing was declared, so an unstyled control's markup is
byte-identical — the guards assert the exact attribute (class="markdown-body"), not Contain, so
a trailing space or an empty class="" fails. One deliberate exception, called out in #1327: CollaborativeMarkdownView previously emitted a trailing space in its class attribute in the default
view mode; the tokens are now joined. A class attribute is a token list, so that space never matched
anything.
Every positive test was verified by falsification — reverting the views makes exactly those tests
fail (5, then 9, then 5), while the backwards-compatibility guards pass with and without the fix. ControlStyleRenderingTest goes 13 → 44 tests, all through the real HtmlRenderer.
NamedAreaView — resolved as deliberately N/A (bucket ⚪), with the reasoning recorded in the
view itself. A NamedAreaControl is a reference to an area, not a box; the loaded branch renders
only <DispatchView> on the resolved control, which applies its own Style/Class. And the blast radius
makes a wrapper actively wrong rather than merely unnecessary: every layout area, tab body and dialog
body reaches the DOM through this view. The only markup it owns is the not-yet-loaded skeleton/spinner
— styling that would apply the declaration to a spinner and then drop it when content lands, the exact
mid-flight restructure the declaration-gating rule exists to prevent. If a call site ever needs it, the
fix is to forward onto RootControl. (fix: the seven views where the box was not the obvious root (#1297, part 3) #1332)
Not addressed: the general "silently ignored" defect
The closing paragraph of this issue asks whether a view that cannot honour a declared Style should say
so. Nothing here does that — after the sweep there is exactly one view that genuinely cannot
(SpacerView: FluentSpacer exposes no Style/Class parameter, and wrapping it would change its flex
behaviour), so a runtime signal would have one true positive and no obvious home. The mechanism that
would actually prevent the 26th instance is a compile-time one — the StandardView<,> constraint in #1333, and the render tests, which now cover 25 views and fail loudly when a view stops emitting.
Flagging it rather than deciding it: that is a scope call for you, not for the sweep.
One more thing the sweep did not touch, recorded here because it would otherwise only exist in #1288's (now merged) audit body.
The audit's "three caveats on a different axis" are still open, and two of them are the same defect I fixed in MeshNodePickerView as part of the 25:
SelectView and DateTimeView are FormComponentBase views that pass Style where every other form view passes ComputedStyle — so their Width/Height are dropped. Style and Class themselves are fine, which is why they sit outside this issue's buckets. One line each; the reason I did not fold them in is that they are not Style/Class drops, and neither is renderable in ControlStyleRenderingTest (form views need a live pointer stream), so they would have ridden along untested inside a PR whose whole claim is falsification-verified coverage.
MenuItemView re-binds DataBind(Skin.Style, x => x.Style)after the base bound ViewModel.Style, so the skin wins and a null skin style nulls the field. Coherent with MenuItemControl.WithStyle(object) routing into the skin, but the inherited WithStyle(Func<StyleBuilder,…>) overload writes the control's Style and is then clobbered. That one is a genuine design question (which of the two overloads is authoritative), not an omission.
Happy to take either as a follow-up — flagging rather than deciding.
All 25 views fixed across three merged PRs — #1323 (the 8 Class-only drops), #1327 (10 views with a literal root), #1332 (7 where the box was not the obvious root).
Nothing gained a wrapper. Every fix lands on an element the view already renders, or forwards the declaration to the control it delegates to. That was the constraint from #1288: a blanket wrapper breaks TodoLayoutAreas.cs:117, which sets flex: 1 on its fragment's own root because that root is the direct flex child — and being in-mesh source, no build or test would have caught it.
Two rules emerged that were not in the original audit and are now pinned by tests:
The author's Style goes last, so a declared max-width overrides the view's default. Dropping it was the bug; letting the default silently win is the same bug with extra steps. Asserted as ordering, not presence.
ControlStyleRenderingTest went 13 → 44 tests, all through the real HtmlRenderer. Every positive test was falsified by reverting its view; the backwards-compat guards assert exact attributes rather than Contain, and pass with and without the fix. One guard was missing a space and therefore could never fail — caught and proven failing by temporarily double-applying the style.
SelectView/DateTimeView (Width/Height) and MenuItemView (skin clobbers Style) — recorded here rather than fixed blind, since they are not Style/Class drops and are not renderable in this harness.
MeshNodePickerView turned out to be one line: FormComponentBase.ComputedStyle already folds Style+Width+Height and the view simply never emitted it — so emitting it recovers all three.
Activity
rbuergi commentedon Aug 12, 2026
Swept, split into three stacked PRs so each stays reviewable. All 25 views fixed, plus the
"unclear" one resolved and the three out-of-bucket ones tracked separately.
Class-only dropsMerge order is #1323 → #1327 → #1332 — each is based on the previous one because they share the
ClassSuffix/StyleSuffixhelpers onBlazorView. GitHub re-targets each tomainautomatically asits base merges.
No blanket wrapper — and no new element anywhere
Not one of the 25 gained a wrapper. Every fix lands on an element the view already renders, chosen
per view; in the two cases where the view has no element of its own the declaration is forwarded to
the control it delegates to (
MeshNodeCardView's embedded area), the shapePropertyView/EditFormViewalready use. So theTodoLayoutAreas.cs:117class of regression —flex: 1on afragment's own root, in in-mesh source no build type-checks — cannot arise from this sweep.
Two rules came out of it that were not in the original audit:
max-widthoverrides the view's default. Dropping the declaration was the bug; letting the default win would be
the same bug with extra steps. Asserted as ordering, not presence.
ViewModel.Style) is specific to the wrapper case and was deliberatelynot copied here: no element is created or removed, so a late-resolving
JsonPointerReferencejustupdates an attribute — it cannot restructure the DOM mid-flight. Applying fix: HtmlView honours Style/Class instead of silently dropping them #1288's rule mechanically
would have been cargo-culting.
Backwards compatibility
Both helpers return the empty string when nothing was declared, so an unstyled control's markup is
byte-identical — the guards assert the exact attribute (
class="markdown-body"), notContain, soa trailing space or an empty
class=""fails. One deliberate exception, called out in #1327:CollaborativeMarkdownViewpreviously emitted a trailing space in its class attribute in the defaultview mode; the tokens are now joined. A class attribute is a token list, so that space never matched
anything.
Every positive test was verified by falsification — reverting the views makes exactly those tests
fail (5, then 9, then 5), while the backwards-compatibility guards pass with and without the fix.
ControlStyleRenderingTestgoes 13 → 44 tests, all through the realHtmlRenderer.The specific questions in this issue
MeshNodePickerView'sWidth/Height— the same fix, and one line.FormComponentBasealready folds Style+Width+Height into
ComputedStyle; this view just never emitted it. Switching itsroot to
style="@ComputedStyle"recovers all three. (fix: the seven views where the box was not the obvious root (#1297, part 3) #1332)NamedAreaView— resolved as deliberately N/A (bucket ⚪), with the reasoning recorded in theview itself. A
NamedAreaControlis a reference to an area, not a box; the loaded branch rendersonly
<DispatchView>on the resolved control, which applies its own Style/Class. And the blast radiusmakes a wrapper actively wrong rather than merely unnecessary: every layout area, tab body and dialog
body reaches the DOM through this view. The only markup it owns is the not-yet-loaded skeleton/spinner
— styling that would apply the declaration to a spinner and then drop it when content lands, the exact
mid-flight restructure the declaration-gating rule exists to prevent. If a call site ever needs it, the
fix is to forward onto
RootControl. (fix: the seven views where the box was not the obvious root (#1297, part 3) #1332)SearchBoxView/MeshSearchView/Monaco/CodeEditorView— a different defect, filed asThree registered control views never bind Style/Class at all — they derive from ComponentBase #1333. Nothing is dropped in markup because nothing is bound: they derive from plain
ComponentBase,so
BlazorView.BindDatanever runs. Fixing it is a view-base/lifecycle question, not an attributeedit, and folding it in here would have turned a markup sweep into a rewrite. Three registered control views never bind Style/Class at all — they derive from ComponentBase #1333 also proposes the
durable guard: constrain
StandardView<,>to aBlazorView<,>-derived component so a fourth instancecannot be registered silently.
Not addressed: the general "silently ignored" defect
The closing paragraph of this issue asks whether a view that cannot honour a declared
Styleshould sayso. Nothing here does that — after the sweep there is exactly one view that genuinely cannot
(
SpacerView:FluentSpacerexposes no Style/Class parameter, and wrapping it would change its flexbehaviour), so a runtime signal would have one true positive and no obvious home. The mechanism that
would actually prevent the 26th instance is a compile-time one — the
StandardView<,>constraint in#1333, and the render tests, which now cover 25 views and fail loudly when a view stops emitting.
Flagging it rather than deciding it: that is a scope call for you, not for the sweep.
rbuergi commentedon Aug 12, 2026
One more thing the sweep did not touch, recorded here because it would otherwise only exist in #1288's (now merged) audit body.
The audit's "three caveats on a different axis" are still open, and two of them are the same defect I fixed in
MeshNodePickerViewas part of the 25:SelectViewandDateTimeViewareFormComponentBaseviews that passStylewhere every other form view passesComputedStyle— so theirWidth/Heightare dropped. Style and Class themselves are fine, which is why they sit outside this issue's buckets. One line each; the reason I did not fold them in is that they are notStyle/Classdrops, and neither is renderable inControlStyleRenderingTest(form views need a live pointer stream), so they would have ridden along untested inside a PR whose whole claim is falsification-verified coverage.MenuItemViewre-bindsDataBind(Skin.Style, x => x.Style)after the base boundViewModel.Style, so the skin wins and a null skin style nulls the field. Coherent withMenuItemControl.WithStyle(object)routing into the skin, but the inheritedWithStyle(Func<StyleBuilder,…>)overload writes the control's Style and is then clobbered. That one is a genuine design question (which of the two overloads is authoritative), not an omission.Happy to take either as a follow-up — flagging rather than deciding.
Merge pull request #1323 from Systemorph/fix/1297-class-only-drops
Merge pull request #1327 from Systemorph/fix/1297-literal-root-views
Merge pull request #1332 from Systemorph/fix/1297-branching-root-views
rbuergi commentedon Aug 12, 2026
All 25 views fixed across three merged PRs — #1323 (the 8
Class-only drops), #1327 (10 views with a literal root), #1332 (7 where the box was not the obvious root).Nothing gained a wrapper. Every fix lands on an element the view already renders, or forwards the declaration to the control it delegates to. That was the constraint from #1288: a blanket wrapper breaks
TodoLayoutAreas.cs:117, which setsflex: 1on its fragment's own root because that root is the direct flex child — and being in-mesh source, no build or test would have caught it.Two rules emerged that were not in the original audit and are now pinned by tests:
Stylegoes last, so a declaredmax-widthoverrides the view's default. Dropping it was the bug; letting the default silently win is the same bug with extra steps. Asserted as ordering, not presence.ControlStyleRenderingTestwent 13 → 44 tests, all through the realHtmlRenderer. Every positive test was falsified by reverting its view; the backwards-compat guards assert exact attributes rather thanContain, and pass with and without the fix. One guard was missing a space and therefore could never fail — caught and proven failing by temporarily double-applying the style.Left deliberately, each with reasoning recorded:
NamedAreaView— resolved as N/A. It is a reference to an area, not a box; every layout area, tab and dialog body renders through it, so a wrapper would be fix: HtmlView honours Style/Class instead of silently dropping them #1288's regression on the busiest path in the app.SpacerView—FluentSpacerexposes no Style/Class at all (pre-existing).ComponentBaseviews (SearchBoxView,MeshSearchView,Monaco/CodeEditorView) → Three registered control views never bind Style/Class at all — they derive from ComponentBase #1333. Different defect: nothing is bound, so nothing is dropped.SelectView/DateTimeView(Width/Height) andMenuItemView(skin clobbers Style) — recorded here rather than fixed blind, since they are not Style/Class drops and are not renderable in this harness.MeshNodePickerViewturned out to be one line:FormComponentBase.ComputedStylealready folds Style+Width+Height and the view simply never emitted it — so emitting it recovers all three.