enlace de formularios de Blazor ASP.NET Core

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 10 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la directiva de compatibilidad de .NET y .NET Core. Para la versión actual, consulte la versión de .NET 10 de este artículo.

En este artículo se explica cómo usar la vinculación en formularios Blazor.

EditForm / EditContext modelo

Un componente EditForm crea un objeto EditContext basado en el objeto asignado como un valor en cascada para otros componentes del formulario. El componente EditContext hace seguimiento de los metadatos del proceso de edición, los incluidos los campos del formulario que se han modificado y los mensajes de validación actuales. La asignación a EditForm.Model o a EditForm.EditContext puede enlazar un formulario a los datos.

Vinculación de modelos

Asignación a EditForm.Model:

<EditForm ... Model="Model" ...>
    ...
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();
}
<EditForm ... Model="Model" ...>
    ...
</EditForm>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();
}

Nota:

La mayoría de los ejemplos del modelo de formulario de este artículo enlazan formularios a propiedades de C#, pero también se admite el enlace de campos de C#.

Vinculación de contexto

Asignación a EditForm.EditContext:

<EditForm ... EditContext="editContext" ...>
    ...
</EditForm>

@code {
    private EditContext? editContext;

    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
    }
}
<EditForm ... EditContext="editContext" ...>
    ...
</EditForm>

@code {
    private EditContext? editContext;

    public Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
    }
}

Asigne un EditContexto un Model a un EditForm. Si se asignan ambos, se produce un error en el momento de ejecutar.

Tipos compatibles

El enlace admite:

  • Tipos primitivos
  • Colecciones
  • Tipos complejos
  • Tipos recursivos
  • Tipos con constructores
  • Enumeraciones

También puede usar los atributos [DataMember] y [IgnoreDataMember] para personalizar el enlace de modelos. Use estos atributos para cambiar el nombre de las propiedades, omitir las propiedades y marcar las propiedades según sea necesario.

Al enlazar un tipo con parámetros de constructor, si un parámetro de constructor coincide con una propiedad por nombre, el parámetro constructor tiene prioridad. El mapeador usa el DataMember.Name explícito de la propiedad, si está presente, como nombre del campo del formulario, pero por lo demás ignora los atributos de mapeo de la propiedad. Los parámetros del constructor son siempre obligatorios.

Opciones de enlace adicionales

Hay disponibles opciones de enlace de modelos adicionales desde RazorComponentsServiceOptions al llamar a AddRazorComponents:

A continuación se muestran los valores predeterminados asignados por el marco:

builder.Services.AddRazorComponents(options =>
{
    options.FormMappingUseCurrentCulture = true;
    options.MaxFormMappingCollectionSize = 1024;
    options.MaxFormMappingErrorCount = 200;
    options.MaxFormMappingKeySize = 1024 * 2;
    options.MaxFormMappingRecursionDepth = 64;
}).AddInteractiveServerComponents();

Nombres de formularios

Use el parámetro FormName para asignar un nombre de formulario. Los nombres de los formularios deben ser únicos para poder enlazar los datos del modelo. El siguiente formulario se denomina RomulanAle:

<EditForm ... FormName="RomulanAle" ...>
    ...
</EditForm>

Proporcionar un nombre de formulario:

  • Es necesario para todos los formularios enviados por componentes del lado servidor representados estáticamente.
  • No es necesario para los formularios enviados por componentes representados de forma interactiva, que incluye formularios en aplicaciones y componentes de Blazor WebAssembly con un modo de representación interactivo. Sin embargo, recomendamos proporcionar un nombre único para cada formulario para evitar errores al enviar formularios en tiempo de ejecución si en algún momento un formulario deja de ser interactivo.

El nombre del formulario solo se comprueba cuando el formulario se publica en un punto de conexión como una solicitud HTTP POST tradicional desde un componente del lado servidor representado estáticamente. El marco no produce una excepción en el punto de representación de un formulario, sino solo en el momento en el que llega un HTTP POST y no especifica un nombre de formulario.

Hay un ámbito de formulario sin nombre (cadena vacía) por encima del componente raíz de la aplicación, lo cual es suficiente cuando no hay conflictos entre nombres de formulario en la aplicación. Si es posible que existan colisiones de nombres de formularios, como cuando se incluye un formulario de una biblioteca y no se tiene control sobre el nombre del formulario utilizado por el desarrollador de la biblioteca, proporciona un ámbito de nombre de formulario con el componente FormMappingScope en el proyecto principal del objeto Blazor Web App.

En el ejemplo siguiente, el componente HelloFormFromLibrary tiene un formulario denominado Hello y está en una biblioteca.

HelloFormFromLibrary.razor:

<EditForm FormName="Hello" Model="this" OnSubmit="Submit">
    <InputText @bind-Value="Name" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Name from the library's form!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    private string? Name { get; set; }

    private void Submit() => submitted = true;
}

El siguiente componente de NamedFormsWithScope usa el componente HelloFormFromLibrary de la biblioteca y también tiene un formulario denominado Hello. El nombre del ámbito del componente FormMappingScope es ParentContext para cualquier formulario proporcionado por el componente HelloFormFromLibrary. Aunque ambos formularios de este ejemplo tienen el mismo nombre de formulario (Hello), los nombres de formulario no entran en conflicto y los eventos se dirigen al formulario correcto en los eventos de envío POST del formulario.

NamedFormsWithScope.razor:

@page "/named-forms-with-scope"

<div>Hello form from a library</div>

<FormMappingScope Name="ParentContext">
    <HelloFormFromLibrary />
</FormMappingScope>

<div>Hello form using the same form name</div>

<EditForm FormName="Hello" Model="this" OnSubmit="Submit">
    <InputText @bind-Value="Name" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Name from the app form!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    private string? Name { get; set; }

    private void Submit() => submitted = true;
}

Proporciona un parámetro del formulario ([SupplyParameterFromForm])

El atributo [SupplyParameterFromForm] indica que el valor de la propiedad asociada debe proporcionarse a partir de los datos del formulario para el formulario. Los datos de la solicitud cuyo nombre coincide con el de la propiedad se vinculan a la propiedad. Las entradas basadas en InputBase<TValue> generan nombres de valor de formulario que coinciden con los nombres que Blazor usa para el enlace de modelos. A diferencia de las propiedades del parámetro de componente ([Parameter]), no es necesario marcar las propiedades anotadas [SupplyParameterFromForm] con public.

BlazorLa asignación de formularios con [SupplyParameterFromForm] no utiliza el enlace de modelos MVC. No se admiten los atributos del Microsoft.AspNetCore.Mvc.ModelBinding espacio de nombres, como [BindNever] y [BindRequired]. No use estos atributos para evitar el sobreposting. En su lugar, use un modelo de formulario dedicado, un modelo de vista o un objeto de transferencia de datos (DTO) que incluya solo las propiedades que los usuarios pueden modificar. Para obtener más información, consulte Mitigación de ataques de sobrepostificación.

Puedes especificar los siguientes parámetros de enlace de formulario para el atributo [SupplyParameterFromForm]:

  • Name: Obtiene o establece el nombre del parámetro. El nombre se usa para determinar el prefijo que se va a usar para buscar coincidencias con los datos del formulario y decidir si es necesario enlazar o no el valor.
  • FormName: Obtiene o establece el nombre del controlador. El nombre se usa para hacer coincidir el parámetro con el formulario por el nombre del formulario y poder así decidir si es necesario enlazar o no el valor.

En el ejemplo siguiente se enlazan de forma independiente dos formularios a sus modelos por nombre de formulario.

Starship6.razor:

@page "/starship-6"
@inject ILogger<Starship6> Logger

<EditForm Model="Model1" OnSubmit="Submit1" FormName="Holodeck1">
    <div>
        <label>
            Holodeck 1 Identifier: 
            <InputText @bind-Value="Model1!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<EditForm Model="Model2" OnSubmit="Submit2" FormName="Holodeck2">
    <div>
        <label>
            Holodeck 2 Identifier: 
            <InputText @bind-Value="Model2!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm(FormName = "Holodeck1")]
    private Holodeck? Model1 { get; set; }

    [SupplyParameterFromForm(FormName = "Holodeck2")]
    private Holodeck? Model2 { get; set; }

    protected override void OnInitialized()
    {
        Model1 ??= new();
        Model2 ??= new();
    }

    private void Submit1() => Logger.LogInformation("Submit1: Id={Id}", Model1?.Id);

    private void Submit2() => Logger.LogInformation("Submit2: Id={Id}", Model2?.Id);

    public class Holodeck
    {
        public string? Id { get; set; }
    }
}
@page "/starship-6"
@inject ILogger<Starship6> Logger

<EditForm Model="Model1" OnSubmit="Submit1" FormName="Holodeck1">
    <div>
        <label>
            Holodeck 1 Identifier: 
            <InputText @bind-Value="Model1!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<EditForm Model="Model2" OnSubmit="Submit2" FormName="Holodeck2">
    <div>
        <label>
            Holodeck 2 Identifier: 
            <InputText @bind-Value="Model2!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm(FormName = "Holodeck1")]
    private Holodeck? Model1 { get; set; }

    [SupplyParameterFromForm(FormName = "Holodeck2")]
    private Holodeck? Model2 { get; set; }

    protected override void OnInitialized()
    {
        Model1 ??= new();
        Model2 ??= new();
    }

    private void Submit1() => Logger.LogInformation("Submit1: Id={Id}", Model1?.Id);

    private void Submit2() => Logger.LogInformation("Submit2: Id={Id}", Model2?.Id);

    public class Holodeck
    {
        public string? Id { get; set; }
    }
}

Anidar y enlazar formularios

En las instrucciones siguientes se muestra cómo anidar y enlazar formularios secundarios.

La siguiente clase de detalles de envío (ShipDetails) contiene una descripción y una longitud para un subformulario.

ShipDetails.cs:

namespace BlazorSample;

public class ShipDetails
{
    public string? Description { get; set; }
    public int? Length { get; set; }
}
namespace BlazorSample;

public class ShipDetails
{
    public string? Description { get; set; }
    public int? Length { get; set; }
}

Los siguientes nombres de clase Ship nombran un identificador (Id) e incluyen los detalles del buque.

Ship.cs:

namespace BlazorSample
{
    public class Ship
    {
        public string? Id { get; set; }
        public ShipDetails Details { get; set; } = new();
    }
}
namespace BlazorSample
{
    public class Ship
    {
        public string? Id { get; set; }
        public ShipDetails Details { get; set; } = new();
    }
}

El siguiente subformulario se usa para editar valores del tipo ShipDetails. Esto se implementa heredando Editor<T> en la parte superior del componente. Editor<T> garantiza que el componente secundario genere los nombres de campo de formulario correctos basados en el modelo (T), donde T en el ejemplo siguiente es ShipDetails.

StarshipSubform.razor:

@inherits Editor<ShipDetails>

<div>
    <label>
        Description: 
        <InputText @bind-Value="Value!.Description" />
    </label>
</div>
<div>
    <label>
        Length: 
        <InputNumber @bind-Value="Value!.Length" />
    </label>
</div>
@inherits Editor<ShipDetails>

<div>
    <label>
        Description: 
        <InputText @bind-Value="Value!.Description" />
    </label>
</div>
<div>
    <label>
        Length: 
        <InputNumber @bind-Value="Value!.Length" />
    </label>
</div>

El formulario principal está enlazado a la clase Ship. El componente StarshipSubform se usa para editar los detalles del envío, enlazados como Model!.Details.

Starship7.razor:

@page "/starship-7"
@inject ILogger<Starship7> Logger

<EditForm Model="Model" OnSubmit="Submit" FormName="Starship7">
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <StarshipSubform @bind-Value="Model!.Details" />
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Ship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => 
        Logger.LogInformation("Id = {Id} Desc = {Description} Length = {Length}",
            Model?.Id, Model?.Details?.Description, Model?.Details?.Length);
}
@page "/starship-7"
@inject ILogger<Starship7> Logger

<EditForm Model="Model" OnSubmit="Submit" FormName="Starship7">
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <StarshipSubform @bind-Value="Model!.Details" />
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Ship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => 
        Logger.LogInformation("Id = {Id} Desc = {Description} Length = {Length}",
            Model?.Id, Model?.Details?.Description, Model?.Details?.Length);
}

Inicialización de datos de formulario con SSR estática

Cuando un componente adopta una SSR estática, el método de ciclo de vida OnInitialized{Async} y el método de ciclo de vida OnParametersSet{Async} se activan cuando el componente se representa inicialmente y en cada formulario POST en el servidor. Para inicializar los valores del modelo de formulario, confirma si el modelo ya tiene datos antes de asignar nuevos valores de modelo en OnParametersSet{Async}, como se muestra en el ejemplo siguiente.

StarshipInit.razor:

@page "/starship-init"
@inject ILogger<StarshipInit> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="StarshipInit">
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    protected override void OnParametersSet()
    {
        if (Model!.Id == default)
        {
            LoadData();
        }
    }

    private void LoadData()
    {
        Model!.Id = "Set by LoadData";
    }

    private void Submit()
    {
        Logger.LogInformation("Id = {Id}", Model?.Id);
    }

    public class Starship
    {
        public string? Id { get; set; }
    }
}

Escenarios de error avanzados del mapeo de formularios

El marco crea una instancia de y rellena el FormMappingContext de un formulario, que es el contexto asociado a la operación de asignación de un formulario determinado. Cada ámbito de asignación (definido por un componente de FormMappingScope) crea instancias de FormMappingContext. Cada vez que un [SupplyParameterFromForm] solicita el contexto de un valor, el marco rellena el FormMappingContext con el valor intentado y los errores de asignación.

No se espera que los desarrolladores interactúen directamente con FormMappingContext, ya que es principalmente un origen de datos para InputBase<TValue>, EditContext y otras implementaciones internas para mostrar errores de asignación como errores de validación. En escenarios personalizados avanzados, los desarrolladores pueden acceder a FormMappingContext directamente como un [CascadingParameter] para escribir código personalizado que consume los valores intentados y los errores de asignación.

InputText basado en el evento de entrada

Use el componente InputText para crear un componente personalizado que use el evento oninput (input) en lugar del evento onchange (change). El uso del evento input desencadena la validación de campos en cada pulsación de tecla.

El componente CustomInputText siguiente hereda el componente InputText del marco y establece el enlace de los eventos en el evento oninput (input).

CustomInputText.razor:

@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />

El componente CustomInputText se puede usar en cualquier lugar donde InputText se use. El componente siguiente usa el componente CustomInputText compartido.

Starship11.razor:

@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <CustomInputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}

Componentes de entrada personalizados

Para escenarios de procesamiento de entrada personalizados, las subsecciones siguientes muestran componentes de entrada personalizados:

Recomendamos que derive sus componentes de entrada personalizados de InputBase<TValue>, a menos que requisitos específicos se lo impidan. El equipo de ASP.NET Core mantiene activamente la clase InputBase<TValue>, lo que garantiza que se mantenga actualizada con las características Blazor y los cambios más recientes del marco.

Componente de entrada basado en InputBase<T>

El siguiente componente de ejemplo:

  • Se hereda de InputBase<TValue>. Los componentes que heredan de InputBase<TValue> deben usarse en un formulario Blazor (EditForm).
  • Acepta una entrada booleana procedente de una casilla de selección.
  • Establece el color de fondo de su contenedor <div> en función del estado de la casilla, que se produce cuando se ejecuta el método AfterChange después del enlace (@bind:after).
  • Es necesario para invalidar el método TryParseValueFromString de la clase base, pero no procesa los datos de entrada de cadena porque una casilla no proporciona datos de cadena. Las implementaciones de ejemplo de TryParseValueFromString para otros tipos de componentes de entrada que procesan la entrada de cadena están disponibles en el origen de referencia de ASP.NET Core.

Nota:

Los vínculos de la documentación al origen de referencia de .NET cargan normalmente la rama predeterminada del repositorio, que representa el desarrollo actual para la próxima versión de .NET. Para seleccionar una etiqueta de una versión específica, usa la lista desplegable Cambiar ramas o etiquetas. Para obtener más información, consulta Procedimientos para seleccionar una etiqueta de versión de código fuente de ASP.NET Core (dotnet/AspNetCore.Docs #26205).

EngineeringApprovalInputDerived.razor:

@using System.Diagnostics.CodeAnalysis
@inherits InputBase<bool>

<div class="@divCssClass">
    <label>
        Engineering Approval:
        <input @bind="CurrentValue" @bind:after="AfterChange" class="@CssClass" 
            type="checkbox" />
    </label>
</div>

@code {
    private string? divCssClass;

    private void AfterChange()
    {
        divCssClass = CurrentValue ? "bg-success text-white" : null;
    }

    protected override bool TryParseValueFromString(
        string? value, out bool result, 
        [NotNullWhen(false)] out string? validationErrorMessage)
            => throw new NotSupportedException(
                "This component does not parse string inputs. " +
                $"Bind to the '{nameof(CurrentValue)}' property, " +
                $"not '{nameof(CurrentValueAsString)}'.");
}

Para utilizar el componente anterior en el formulario de ejemplo de la nave espacial (Starship3.razor/Starship.cs), sustituye el bloque <div> correspondiente al campo de aprobación de ingeniería por una instancia del componente EngineeringApprovalInputDerived enlazada a la propiedad IsValidatedDesign del modelo:

- <div>
-     <label>
-         Engineering Approval: 
-         <InputCheckbox @bind-Value="Model!.IsValidatedDesign" />
-     </label>
- </div>
+ <EngineeringApprovalInputDerived @bind-Value="Model!.IsValidatedDesign" />

Si el componente que hereda de InputBase<TValue> se renderiza estáticamente, asigne la propiedad InputBase<TValue>.NameAttributeValue al atributo name de elementos <input>:

<input @bind="CurrentValue" @bind:after="AfterChange" class="@CssClass"
    type="checkbox" name="@NameAttributeValue" />

La asignación anterior no es necesaria si se garantiza que el componente siempre se represente de forma interactiva.

Componente de entrada con control de desarrollador total

El siguiente componente de ejemplo:

  • No hereda de InputBase<TValue>. El componente asume el control total del procesamiento de las entradas, incluida la vinculación, los callbacks y la validación. El componente se puede usar dentro o fuera de un formulario Blazor (EditForm).
  • Acepta una entrada booleana procedente de una casilla de selección.
  • Cambia el color de fondo si la casilla está activada.

El código del componente incluye:

  • La propiedad Value se usa con enlace bidireccional para obtener o establecer el valor de la entrada. ValueChanged es la llamada de retorno que actualiza el valor vinculado.

  • Cuando se usa en un formulario Blazor:

    • EditContext es un valor en cascada.
    • fieldCssClass diseña el campo en función del resultado de la validación EditContext.
    • ValueExpression es una expresión (Expression<Func<T>>) asignada por el marco que identifica el valor enlazado.
    • FieldIdentifier identifica de forma única un solo campo que se puede editar, normalmente correspondiente a una propiedad de modelo. El identificador de campo se crea con la expresión que identifica el valor enlazado (ValueExpression).
  • En el controlador de eventos OnChange:

    • El valor de la entrada de la casilla de selección se obtiene de InputFileChangeEventArgs.
    • Se establecen el color de fondo y el color de texto del elemento <div> de contenedor.
    • EventCallback.InvokeAsync invoca el delegado asociado al enlace y envía una notificación de evento a los consumidores indicando que el valor ha cambiado.
    • Si el componente se usa en EditForm (la propiedad EditContext no es null), se lla a a EditContext.NotifyFieldChanged para desencadenar la validación.

EngineeringApprovalInputStandalone.razor:

@using System.Globalization
@using System.Linq.Expressions

<div class="@divCssClass">
    <label>
        Engineering Approval:
        <input class="@fieldCssClass" @onchange="OnChange" type="checkbox" 
            value="@Value" />
    </label>
</div>

@code {
    private string? divCssClass;
    private FieldIdentifier fieldIdentifier;
    private string? fieldCssClass => EditContext?.FieldCssClass(fieldIdentifier);

    [CascadingParameter]
    private EditContext? EditContext { get; set; }

    [Parameter]
    public bool? Value { get; set; }

    [Parameter]
    public EventCallback<bool> ValueChanged { get; set; }

    [Parameter]
    public Expression<Func<bool>>? ValueExpression { get; set; }

    protected override void OnInitialized()
    {
        fieldIdentifier = FieldIdentifier.Create(ValueExpression!);
    }

    private async Task OnChange(ChangeEventArgs args)
    {
        BindConverter.TryConvertToBool(args.Value, CultureInfo.CurrentCulture, 
            out var value);

        divCssClass = value ? "bg-success text-white" : null;

        await ValueChanged.InvokeAsync(value);
        EditContext?.NotifyFieldChanged(fieldIdentifier);
    }
}

Para usar el componente anterior en el formulario de ejemplo de nave estelar (Starship3.razor/Starship.cs), reemplaza el bloque <div> del campo de aprobación de ingeniería por una instancia del componente EngineeringApprovalInputStandalone enlazada a la propiedad IsValidatedDesign del modelo:

- <div>
-     <label>
-         Engineering Approval: 
-         <InputCheckbox @bind-Value="Model!.IsValidatedDesign" />
-     </label>
- </div>
+ <EngineeringApprovalInputStandalone @bind-Value="Model!.IsValidatedDesign" />

El componente EngineeringApprovalInputStandalone también funciona fuera de un objeto EditForm:

<EngineeringApprovalInputStandalone @bind-Value="ValidDesign" />

<div>
    <b>ValidDesign:</b> @ValidDesign
</div>

@code {
    private bool ValidDesign { get; set; }
}

Botones de radio

El ejemplo de esta sección se basa en el formulario Starfleet Starship Database (componente Starship3) de la sección Formulario de ejemplo de este artículo.

Agrega los siguientes tipos enum a la aplicación. Crea un nuevo archivo para que los contenga o agrégalos al archivo Starship.cs.

public class ComponentEnums
{
    public enum Manufacturer { SpaceX, NASA, ULA, VirginGalactic, Unknown }
    public enum Color { ImperialRed, SpacecruiserGreen, StarshipBlue, VoyagerOrange }
    public enum Engine { Ion, Plasma, Fusion, Warp }
}

Haz que la clase ComponentEnums sea accesible al:

  • Modelo Starship en Starship.cs (por ejemplo, using static ComponentEnums;).
  • Formulario Starfleet Starship Database (Starship3.razor) (por ejemplo, @using static ComponentEnums).

Usa componentes InputRadio<TValue> con el componente InputRadioGroup<TValue> para crear un grupo de botones de radio. En el ejemplo siguiente, las propiedades se agregan al modelo de Starship descrito en la sección Formulario ejemplo del artículo Componentes de entrada:

[Required]
[Range(typeof(Manufacturer), nameof(Manufacturer.SpaceX), 
    nameof(Manufacturer.VirginGalactic), ErrorMessage = "Pick a manufacturer.")]
public Manufacturer Manufacturer { get; set; } = Manufacturer.Unknown;

[Required, EnumDataType(typeof(Color))]
public Color? Color { get; set; } = null;

[Required, EnumDataType(typeof(Engine))]
public Engine? Engine { get; set; } = null;

Actualiza el componente Starship3 de formulario (Starfleet Starship Database) de la sección Formulario de ejemplo del artículo Componentes de entrada. Agrega los componentes para generar lo siguiente:

  • Un grupo de botones de radio para el fabricante del envío.
  • Un grupo de botones de radio anidados para el motor y el color de envío.

Nota:

Los grupos de botones de radio anidados no se suelen usar en formularios porque pueden dar lugar a un diseño desorganizado de controles de formulario que pueden confundir a los usuarios. Sin embargo, hay casos en los que tienen sentido en el diseño de la interfaz de usuario, como en el ejemplo siguiente, que empareja recomendaciones para dos datos introducidos por el usuario: el motor del barco y el color del barco. La validación del formulario requiere un motor y un color. El diseño del formulario usa objetos anidados InputRadioGroup<TValue> para emparejar las recomendaciones de motor y color. Sin embargo, el usuario puede combinar cualquier motor con cualquier color para enviar el formulario.

Nota:

Asegúrate de que la clase ComponentEnums esté disponible para el componente para el ejemplo siguiente:

@using static ComponentEnums
<fieldset>
    <legend>Manufacturer</legend>
    <InputRadioGroup @bind-Value="Model!.Manufacturer">
        @foreach (var manufacturer in Enum.GetValues<Manufacturer>())
        {
            <div>
                <label>
                    <InputRadio Value="manufacturer" />
                    @manufacturer
                </label>
            </div>
        }
    </InputRadioGroup>
</fieldset>

<fieldset>
    <legend>Engine and Color</legend>
    <p>
        Engine and color pairs are recommended, but any
        combination of engine and color is allowed.
    </p>
    <InputRadioGroup Name="engine" @bind-Value="Model!.Engine">
        <InputRadioGroup Name="color" @bind-Value="Model!.Color">
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Ion" />
                        Ion
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.ImperialRed" />
                        Imperial Red
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Plasma" />
                        Plasma
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.SpacecruiserGreen" />
                        Spacecruiser Green
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Fusion" />
                        Fusion
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.StarshipBlue" />
                        Starship Blue
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Warp" />
                        Warp
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.VoyagerOrange" />
                        Voyager Orange
                    </label>
                </div>
            </div>
        </InputRadioGroup>
    </InputRadioGroup>
</fieldset>

Nota:

Si se omite Name, los componentes de InputRadio<TValue> se agrupan según su antecesor más reciente.

Si implementaste el marcado Razor anterior en el componente Starship3 de la sección Formulario de ejemplo del artículo Componentes de entrada, actualiza el registro del método Submit:

Logger.LogInformation("Id = {Id} Description = {Description} " +
    "Classification = {Classification} MaximumAccommodation = " +
    "{MaximumAccommodation} IsValidatedDesign = " +
    "{IsValidatedDesign} ProductionDate = {ProductionDate} " +
    "Manufacturer = {Manufacturer}, Engine = {Engine}, " +
    "Color = {Color}",
    Model?.Id, Model?.Description, Model?.Classification,
    Model?.MaximumAccommodation, Model?.IsValidatedDesign,
    Model?.ProductionDate, Model?.Manufacturer, Model?.Engine, 
    Model?.Color);

Al trabajar con botones de radio en un formulario, el enlace de datos se controla de manera diferente que otros elementos, ya que los botones de radio se evalúan como un grupo. El valor de cada botón de radio es fijo, pero el valor del grupo de botones de radio es el valor del botón de radio seleccionado. El ejemplo siguiente muestra cómo:

  • Controlar el enlace de datos en un grupo de botones de radio
  • Admitir la validación usando un componente InputRadio<TValue> personalizado

InputRadio.razor:

@using System.Globalization
@inherits InputBase<TValue>
@typeparam TValue

<input @attributes="AdditionalAttributes" type="radio" value="@SelectedValue" 
       checked="@(SelectedValue.Equals(Value))" @onchange="OnChange" />

@code {
    [Parameter]
    public TValue SelectedValue { get; set; }

    private void OnChange(ChangeEventArgs args)
    {
        CurrentValueAsString = args.Value.ToString();
    }

    protected override bool TryParseValueFromString(string value, 
        out TValue result, out string errorMessage)
    {
        var success = BindConverter.TryConvertTo<TValue>(
            value, CultureInfo.CurrentCulture, out var parsedValue);
        if (success)
        {
            result = parsedValue;
            errorMessage = null;

            return true;
        }
        else
        {
            result = default;
            errorMessage = "The field isn't valid.";

            return false;
        }
    }
}

Para obtener más información sobre los parámetros de tipo genérico (@typeparam), consulta los siguientes artículos:

Usa el siguiente modelo de ejemplo.

StarshipModel.cs:

using System.ComponentModel.DataAnnotations;

namespace BlazorServer80
{
    public class Model
    {
        [Range(1, 5)]
        public int Rating { get; set; }
    }
}

El componente RadioButtonExample siguiente usa el componente InputRadio anterior para obtener y validar una clasificación del usuario:

RadioButtonExample.razor:

@page "/radio-button-example"
@using System.ComponentModel.DataAnnotations
@using Microsoft.Extensions.Logging
@inject ILogger<RadioButtonExample> Logger

<h1>Radio Button Example</h1>

<EditForm Model="Model" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    @for (int i = 1; i <= 5; i++)
    {
        <div>
            <label>
                <InputRadio name="rate" SelectedValue="i" 
                    @bind-Value="Model.Rating" />
                @i
            </label>
        </div>
    }

    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>@Model.Rating</div>

@code {
    public StarshipModel Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");
    }
}