Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

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.

Newsletter.razor
<Form Model="Model" FormName="newsletter" OnValidSubmit="Save">
    <CheckboxGroup @bind-Value="Model!.Topics" Items="Enum.GetValues<Topic>()" />
</Form>

@code {
    [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, and OnInitialized sets 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:

Newsletter.razor
<Form Model="Model" FormName="newsletter" OnValidSubmit="Save">
    <CheckboxGroup @bind-Value="Model!.Topics" Items="Enum.GetValues<Topic>()" />
</Form>

@code {
    [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.

csharp
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

Email me about

Code

EmptySelection.razor
@using 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>

@code {
    [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,
    }
}

Off: 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.