dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.36k stars 9.99k forks source link

Blazor InputSelect with multiple enabled throws NullRefException if nothing is selected #54431

Open Tornhoof opened 7 months ago

Tornhoof commented 7 months ago

Is there an existing issue for this?

Describe the bug

If there is an InputSelect in an EditForm with multiple values (via array) and nothing is selected (or everything is deselected) the following exception is thrown:

System.NullReferenceException: Object reference not set to an instance of an object.

   at Microsoft.AspNetCore.Components.BindConverter.FormatterDelegateCache.<>c__DisplayClass3_0`1.<MakeArrayFormatter>g__FormatArrayValue|0(T[] value, CultureInfo culture)

   at Microsoft.AspNetCore.Components.Forms.InputSelect`1.BuildRenderTree(RenderTreeBuilder builder)

   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)

Assuming the following model

{
        public List<(bool, string)> CurrentRoles { get; set; } = new List<(bool, string)>();
        public string[] SelectedRoles { get; set; } = [];
}

And the following markup:

        <div class="form-group">
            <label for="roles">Roles</label>
            <InputSelect id="roles" class="form-control" @bind-Value="Model.SelectedRoles">
                @if(Model.CurrentRoles is not null)
                {
                    @foreach (var (selected, role) in Model.CurrentRoles)
                    {
                        <option selected="@selected">@role</option>
                    }
                }
            </InputSelect>
        </div>

Repo see below, markup is here: https://github.com/Tornhoof/InputMultiSelectRepro/blob/main/Components/Pages/Home.razor

Expected Behavior

No such exception

Workaround is to set the array to [] in OnInitializedAsync

Steps To Reproduce

  1. checkout https://github.com/Tornhoof/InputMultiSelectRepro
  2. make sure line 76 (Model.SelectedRoles ??= []) in Home.razor is commented
  3. Start project
  4. Deselect FirstRole in the Input Select Field

Exceptions (if any)

System.NullReferenceException: Object reference not set to an instance of an object.

   at Microsoft.AspNetCore.Components.BindConverter.FormatterDelegateCache.<>c__DisplayClass3_0`1.<MakeArrayFormatter>g__FormatArrayValue|0(T[] value, CultureInfo culture)

   at Microsoft.AspNetCore.Components.Forms.InputSelect`1.BuildRenderTree(RenderTreeBuilder builder)

   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)

--- End of stack trace from previous location ---

   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()

--- End of stack trace from previous location ---

   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()

   at Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(Int32 componentId, RenderFragment renderFragment)

   at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()

   at Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync()

   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()

   at Microsoft.AspNetCore.Components.Rendering.ComponentState.SetDirectParameters(ParameterView parameters)

   at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderRootComponentAsync(Int32 componentId, ParameterView initialParameters)

   at Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(IComponent component, ParameterView initialParameters)

   at Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.RenderEndpointComponent(HttpContext httpContext, Type rootComponentType, ParameterView parameters, Boolean waitForQuiescence)

   at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)

   at Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)

   at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c.<<InvokeAsync>b__10_0>d.MoveNext()

--- End of stack trace from previous location ---

   at Microsoft.AspNetCore.Antiforgery.Internal.AntiforgeryMiddleware.InvokeAwaited(HttpContext context)

   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)

   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

.NET Version

8.0.200

Anything else?

No response

gragra33 commented 7 months ago

You may need nullable:

 public string[]? SelectedRoles { get; set; } = [];
Tornhoof commented 7 months ago

You may need nullable:

Yeah, I tried that, doesn't help, still throws the same exception.

gragra33 commented 7 months ago

@Tornhoof The Binding for the InputSelect with no items selected is thowing the exception in the internal BindConverter class. The MakeArrayFormatter method is expecting a non-nullable array. You need to add Validation to disallow no selection. You can read more about Validation here: Determine if a form field is valid

Tornhoof commented 7 months ago

@gragra33 I'm aware that I can limit the selection to need more than 0 selections, all the examples (and tests afaik) do that. But that kinda defeats the point of it :) I'm also aware that I can cheat with some empty string value or conceptionally None Item or something similar, but that also defeats the whole point. I'm also aware that I can cheat with writing my own component, use input with multiple directly or other methods to bypass the issue or some other method.

My workaround, as described above, is a lot cleaner than those though.

There is a lot of strange (atleast for me) documentation around null and model binding, in this case I simply expect, that for an empty selection input, it creates an empty array for the model binding. I personally think, that empty selections were simply missed during development.

Tornhoof commented 7 months ago

Note: In my attempts to repro #54432 I found out, that the issue only happens for SSR, as soon as @rendermode InteractiveServer is added the array is not null, but empty.