RadioButton/CheckBox + IncludeInPage bug in DotVVM.Controls.Bootstrap4 5.0.0

Hey guys, I updated my packages DotVVM 5.0.1 and Bootstrat4 5.0.0 and had a page crash out with a bs radio button control. I use claude code and had it help prepare an .md file describing my issue in detail (copied herein as I could not attach it). Hope it helps! Keep up the good work guys, I do love DotVVM :slight_smile:

Cheers/Ben

## RadioButton/CheckBox with `IncludeInPage=“false”` throws “Cannot call HtmlWriter.RenderEndTag, since attributes were added into the writer” (DotVVM.Controls.Bootstrap4 5.0.0)

### Environment

- DotVVM: 5.0.1

- DotVVM.Controls.Bootstrap4: 5.0.0

- DotVVM.Contrib.Select2: 5.0.0

- DotVVM.Contrib.LoadablePanel: 5.0.0

- DotVVM.Contrib.BootstrapColorpicker: 5.0.0

- DotVVM.HotReload.AspNetCore: 5.0.1

- Target framework: net10.0

- .NET SDK: 10.0.400

- Hosting: ASP.NET Core (Microsoft.NET.Sdk.Web)

- Upgraded from DotVVM 4.3.9 / DotVVM.Controls.Bootstrap4 4.3.0, where this markup worked correctly.

### Summary

Any `bs:RadioButton` or `bs:CheckBox` with an `IncludeInPage` value binding that evaluates to `false` on the server crashes the whole page render with:

```

DotVVM.Framework.Controls.DotvvmControlException: Error occurred in HtmlGenericControl.Render method

—> System.InvalidOperationException: Cannot call HtmlWriter.RenderEndTag, since attributes were added into the writer. Attributes: class

at DotVVM.Framework.Controls.HtmlWriter.RenderEndTag()

at DotVVM.Framework.Controls.DotvvmControl.RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)

at DotVVM.Framework.Controls.DotvvmControl.Render(IHtmlWriter writer, IDotvvmRequestContext context)

...

```

The exception’s `Control` context points at an *unrelated* ancestor element (in our case, `bs:FormGroup`'s content-column wrapper `

`), which made this confusing to track down - the actual bug is in the sibling `RadioButton`/`CheckBox`, not the element named in the exception.

### Root cause

Decompiling `DotVVM.Controls.Bootstrap4` 5.0.0 (`DotVVM.Framework.Controls.Bootstrap.dll`), both `RadioButton.RenderControl()` and `CheckBox.RenderControl()` override the low-level `RenderControl` extension point instead of `AddAttributesToRender`, and call `writer.AddAttribute(“class”, …)` **unconditionally, before** deferring to the base implementation:

```csharp

// DotVVM.Framework.Controls.Bootstrap4.RadioButton

protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)

{

writer.AddAttribute("class", base.Classes.RadioButton.GetRadioButtonStyle(FormControlStyle), true, null);

if (FormControlStyle == BootstrapFormStyle.BootstrapCustom)

    writer.AddAttribute("class", base.Classes.RadioButton.RadioButtonBootstrapCustom, true, null);

if (IsInline)

    writer.AddAttribute("class", base.Classes.RadioButton.GetRadioButtonInlineStyle(FormControlStyle), true, null);



((DotvvmControl)this).RenderControl(writer, context); // base.RenderControl - this is where IncludeInPage is actually checked

}

```

`CheckBox.RenderControl()` has the identical shape.

In DotVVM 5’s core (`DotvvmControl.RenderControl`), `IncludeInPage` is evaluated *inside* the base implementation, not before it’s called:

```csharp

protected virtual void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)

{

RenderState r = default;

foreach (var (prop, value) in properties)

    TouchProperty(prop, value, ref r);



if (RenderBeforeControl(in r, writer, context))

{

    if (r.IncludeInPage is not IValueBinding) return;

    RenderAsTemplate<ValueTuple>(in r, writer, context); // <-- new in 5.0: renders into an \*isolated\* writer/StringWriter as a knockout template

}

else

{

    AddAttributesToRender(writer, context);

    RenderBeginTag(writer, context);

    RenderContents(writer, context);

    RenderEndTag(writer, context);

    RenderAfterControl(in r, writer);

}

}

```

When `IncludeInPage` evaluates to `false`, DotVVM 5 now renders the control as a Knockout template resource using a brand-new `HtmlWriter` wrapping a `StringWriter` (`RenderAsTemplate`), rather than touching the *outer* writer at all - this is the new “no more flash of uninitialized content” behavior.

The problem: `RadioButton`/`CheckBox` already called `writer.AddAttribute(“class”, …)` on the **outer** writer before `base.RenderControl` ran, i.e. before it knew `IncludeInPage` would take the `RenderAsTemplate` branch. Since `RenderAsTemplate` never opens a tag on the *outer* writer (it only opens tags on its own private `templateWriter`), that queued `class` attribute is never flushed and stays stuck in the outer writer’s pending-attributes list.

Rendering then continues normally with the RadioButton/CheckBox’s siblings, until the *next* ancestor element on the outer writer calls `RenderEndTag()` (in our repro, the enclosing `bs:FormGroup`'s content `

`) - `HtmlWriter.RenderEndTag()` calls `ThrowIfAttributesArePresent()` first, finds the orphaned `class` attribute, and throws.

This appears to affect any `RadioButton`/`CheckBox` with `IncludeInPage` bound to a value that can be `false`. Other Bootstrap4 controls we checked (`Button`, `LinkButton`, `DropDownButton`, `ComboBox`, `TextBox`, `RouteLink`, `ButtonGroup`, `InputGroup`, `Alert`, `Badge`) implement `AddAttributesToRender` instead and don’t have this pattern, so they appear unaffected - `RadioButton`/`CheckBox` (both derived from `CheckableControlBase`) may be the only ones with this specific issue, but it’s worth checking any other control that overrides `RenderControl` directly.

### Minimal repro

```dothtml

<bs:Form Type=“Horizontal” LabelSizeMD=“2”>

<bs:FormGroup LabelText="Options">

    <bs:RadioButton IncludeInPage="{value: SomeNullableThing != null}"

                    CheckedItem="{value: SelectedOption}"

                    CheckedValue="opt1"

                    Text="Option 1" />

    <bs:RadioButton CheckedItem="{value: SelectedOption}"

                     CheckedValue="opt2"

                     Text="Option 2" />

</bs:FormGroup>

</bs:Form>

```

with a view model where `SomeNullableThing` is `null` on first render. Loading the page throws the exception above.

### Workaround

Move `IncludeInPage` off the `RadioButton`/`CheckBox` itself and onto a wrapping element that doesn’t have this bug (e.g. a plain `` or `dot:Decorator`):

```dothtml

<bs:RadioButton CheckedItem="{value: SelectedOption}" CheckedValue="opt1" Text="Option 1" />

```

This works because the `RadioButton` no longer has its own `IncludeInPage`, so it always takes the normal (non-template) render path and its directly-added `class` attribute gets flushed correctly by its own `RenderBeginTag`.

### Suggested fix

`RadioButton.RenderControl()` / `CheckBox.RenderControl()` should add their `class` attributes via an `AddAttributesToRender` override instead of directly on the writer inside `RenderControl`, so they’re only ever added on whichever writer (outer or template) the base implementation actually decides to use.

We’ve just published Bootstrap 4 for DotVVM version 5.0.1, which should fix the issue. Please try it and let us know.