An empty selection comes back with its defaults
Set default choices only on the first load, not after a post, and read a missing list as empty.
Problem
On a static page, a reader unchecks every box in a CheckboxGroup and saves. The save handler
gets the defaults instead, so they're saved and the page comes back with those boxes checked again. In a form with other fields, the list
arrives as null instead, and code that reads .Count throws. A
MultiSelect with nothing selected does the same.
<Form Model="Model" FormName="newsletter" OnValidSubmit="Save">
<CheckboxGroup @bind-Value="Model!.Topics" Items="Enum.GetValues<Topic>()" />
</Form>
{
[SupplyParameterFromForm(FormName = "newsletter")]
private Subscription? Model { get; set; }
protected override void OnInitialized() =>
Model ??= new() { Topics = [Topic.Releases] };
}Cause
Browsers post nothing for an unchecked box or an empty multiple select, so the post has no field for the list at all. What Blazor does next depends on the rest of the form:
- Nothing else posts. Blazor doesn't bind the model, so the property stays
null, andOnInitializedsets the defaults again. - Other fields post. Blazor creates the model and sets the missing list to
null, replacing any list the class starts with.
Checkbox and Switch don't have this problem: a hidden "false" follows each box, so an
unchecked one still posts. A disabled field posts nothing, like any disabled control.
Fix
Give the defaults only when the request isn't a post, and treat null as an empty list wherever you
read it:
<Form Model="Model" FormName="newsletter" OnValidSubmit="Save">
<CheckboxGroup @bind-Value="Model!.Topics" Items="Enum.GetValues<Topic>()" />
</Form>
{
[SupplyParameterFromForm(FormName = "newsletter")]
private Subscription? Model { get; set; }
[CascadingParameter] private HttpContext? HttpContext { get; set; }
protected override void OnInitialized() =>
Model ??= HttpContext?.Request.Method == "POST"
? new()
: new() { Topics = [Topic.Releases] };
private IReadOnlyList<Topic> Chosen => Model!.Topics ?? [];
}
On a page with more than one form, a post from another form is a POST too, and this one would come back with
nothing checked. Check that the post is this form's: Blazor posts the form's name as _handler.
var posted = HttpContext?.Request.HasFormContentType == true
&& HttpContext.Request.Form["_handler"] == "newsletter";
Model ??= posted ? new() : new() { Topics = [Topic.Releases] };Uncheck every box and save, in each mode:
Preview
Code
Microsoft.AspNetCore.Http
<Form Model="Model" FormName="newsletter" OnValidSubmit="Save" MaxWidth="28.Rem" Grow>
<CheckboxGroup @bind-Value="Model!.Topics" Items="Enum.GetValues<Topic>()" Label="Email me about" />
<Row Gap="3" Align="Align.Center" Wrap>
<Button Submit>Save</Button>
if (_saved)
{
<TextBlock Size="TextSize.Sm" Color="Color.Muted">
Saved: (Chosen.Count == 0 ? "no emails" : string.Join(", ", Chosen)).
</TextBlock>
}
</Row>
</Form>
{
[SupplyParameterFromForm(FormName = "newsletter")]
private Subscription? Model { get; set; }
[CascadingParameter] private HttpContext? HttpContext { get; set; }
private bool _saved;
// Defaults unless this form posted: a post with no box checked sends nothing, so Blazor leaves
// Model null, and defaults set here would check the box again.
protected override void OnInitialized()
{
// This form's own post: another form on the page posts too.
var posted = HttpContext?.Request.HasFormContentType == true
&& HttpContext.Request.Form["_handler"] == "newsletter";
Model ??= posted ? new() : new() { Topics = [Topic.Releases] };
}
// Null when nothing was checked: after an empty post, and when other fields post without it.
private IReadOnlyList<Topic> Chosen => Model!.Topics ?? [];
private void Save() => _saved = true;
public sealed class Subscription
{
public List<Topic>? Topics { get; set; }
}
public enum Topic
{
Releases,
Tips,
Events,
}
}Static: Uncheck every box and save: nothing posts for the group, and no default comes back.
On an interactive page nothing is posted: the list is empty rather than null, and the defaults
apply on the first render. The same code works in every mode.
Related
- CheckboxGroup and MultiSelect: what each one posts.
- Forms and validation: forms that post on static pages.
- A form's post fails or arrives empty: how a post finds its model.