Associação de formulários do ASP.NET Core Blazor

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Este artigo explica como usar a associação em formulários Blazor.

EditForm/EditContext model

Um EditForm cria um EditContext com base na instância de modelo atribuída como um valor em cascata para outros componentes no formulário. O EditContext rastreia metadados sobre o processo de edição, incluindo quais campos foram modificados e as mensagens de validação atuais. A atribuição a um EditForm.Modelou a um EditForm.EditContext pode associar um formulário a dados.

Model binding

Atribuição 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();
}

Observação

A maioria dos exemplos de modelo de formulário deste artigo associa formulários a propriedades C#, mas também há suporte para associação de campo C#.

Associação de contexto

Atribuição 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);
    }
}

A atribuição de um EditContext ou de um Model a um EditForm. Se ambos forem atribuídos, um erro de runtime será gerado.

Tipos com suporte

A associação dá suporte a:

  • Tipos primitivos
  • Coleções
  • Tipos complexos
  • Tipos recursivos
  • Tipos com construtores
  • Enums

Você também pode usar os atributos [DataMember] e [IgnoreDataMember] para personalizar a associação de modelo. Use esses atributos para renomear propriedades, ignorar propriedades e marcar propriedades conforme necessário.

Opções de associação adicionais

Opções de associação de modelo adicionais estão disponíveis de RazorComponentsServiceOptions ao chamar AddRazorComponents:

Veja a seguir os valores padrão atribuídos pela estrutura:

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

Nomes de formulário

Use o parâmetro FormName para atribuir um nome de formulário. Os nomes de formulário devem ser exclusivos para associar dados de modelo. O formulário a seguir é chamado RomulanAle:

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

Fornecendo um nome de formulário:

  • É necessário em todos os formulários enviados por componentes do servidor renderizados estaticamente.
  • Não é necessário para formulários enviados por componentes renderizados interativamente, o que inclui formulários em aplicativos Blazor WebAssembly e componentes com um modo de renderização interativo. No entanto, recomendamos fornecer um nome de formulário exclusivo para cada formulário a fim de evitar erros de postagem de formulário de runtime se a interatividade for removida em um formulário.

O nome do formulário só é verificado quando o formulário é postado em um ponto de extremidade como uma solicitação HTTP POST tradicional de um componente do servidor renderizado estaticamente. A estrutura não gera uma exceção no ponto de renderização de um formulário, mas apenas no ponto em que um HTTP POST chega e não especifica um nome de formulário.

Há um escopo de formulário sem nome (cadeia de caracteres vazia) acima do componente raiz do aplicativo, o que é suficiente quando não há colisões de nomes de formulário no aplicativo. Se houver possibilidade de colisões de nomes de formulário, como ao incluir um formulário a partir de uma biblioteca e você não tiver controle do nome do formulário usado pelo desenvolvedor da biblioteca, forneça um escopo de nome de formulário com o componente FormMappingScope no projeto principal do Blazor Web App.

No exemplo a seguir, o componente HelloFormFromLibrary tem um formulário nomeado Hello que está em uma 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;
}

O componente NamedFormsWithScope a seguir usa o componente HelloFormFromLibrary da biblioteca e também tem um formulário chamado Hello. O nome do escopo do componente FormMappingScope é ParentContext para todos os formulários fornecidos pelo componente HelloFormFromLibrary. Embora ambos os formulários neste exemplo tenham o nome do formulário (Hello), os nomes de formulário não colidem e os eventos são roteados para o formulário correto para eventos POST de formulário.

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;
}

Fornecer um parâmetro do formulário ([SupplyParameterFromForm])

O atributo [SupplyParameterFromForm] indica que o valor da propriedade associada deve ser fornecido dos dados do formulário para o formulário. Os dados na solicitação que correspondem ao nome da propriedade estão associados à propriedade. Entradas baseadas em InputBase<TValue> gera de nomes de valor de formulário que correspondem aos nomes que o Blazor usa para associação de modelo. Ao contrário das propriedades do parâmetro do componente ([Parameter]), as propriedades anotadas com [SupplyParameterFromForm] não precisam ser marcadas public.

Você pode especificar os seguintes parâmetros de associação de formulário para o atributo[SupplyParameterFromForm]:

  • Name: obtém ou define o nome do parâmetro. O nome é usado para determinar o prefixo a ser usado para corresponder aos dados do formulário e decidir se o valor precisa ou não ser associado.
  • FormName: obtém ou define o nome do manipulador. O nome é usado para corresponder o parâmetro ao formulário pelo nome do formulário para decidir se o valor precisa ou não ser associado.

O exemplo a seguir associa independentemente dois formulários a seus modelos por nome de formulário.

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; }
    }
}

Aninhar e associar formulários

As diretrizes a seguir demonstram como aninhar e associar formulários filho.

A classe de detalhes da nave a seguir (ShipDetails) contém uma descrição e o comprimento para um subformulário.

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; }
}

A classe Ship a seguir nomeia um identificador (Id) e inclui os detalhes da nave.

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();
    }
}

O subformulário a seguir é usado para editar valores do tipo ShipDetails. Isso é implementado herdando Editor<T> na parte superior do componente. Editor<T> garante que o componente filho gere os nomes de campo de formulário corretos com base no modelo (T), em que T no exemplo a seguir é 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>

O formulário principal é associado à classe Ship. O componente StarshipSubform é usado para editar detalhes da nave, associados 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);
}

Inicializar dados de formulário com SSR estático

Quando um componente adota SSR estático, o método de ciclo de vida OnInitialized{Async} e o método de ciclo de vida OnParametersSet{Async} são acionados quando o componente é inicialmente renderizado e em cada formulário POST para o servidor. Para inicializar os valores do modelo de formulário, confirme se o modelo já tem dados antes de atribuir novos valores de modelo em OnParametersSet{Async}, como demonstra o exemplo a seguir.

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; }
    }
}

Cenários avançados de erro de mapeamento de formulário

A estrutura cria uma instância e preenche o FormMappingContext para um formulário, que é o contexto associado à operação de mapeamento de um determinado formulário. Cada escopo de mapeamento (definido por um componenteFormMappingScope) cria uma instância FormMappingContext. Cada vez que um [SupplyParameterFromForm] solicita um valor ao contexto, a estrutura preenche o FormMappingContext com o valor tentado e quaisquer erros de mapeamento.

Não se espera que os desenvolvedores interajam com FormMappingContext diretamente, pois ele é principalmente uma fonte de dados para InputBase<TValue>, EditContext e outras implementações internas para mostrar erros de mapeamento como erros de validação. Em cenários personalizados avançados, os desenvolvedores podem acessar FormMappingContext diretamente como um [CascadingParameter] para escrever um código personalizado que consome os valores tentados e erros de mapeamento.

Componentes personalizados de entrada

Para cenários de processamento de entrada personalizada, as subseções a seguir demonstram componentes de entrada personalizados:

Recomendamos que você derive seus componentes de entrada personalizados de InputBase<TValue>, a menos que requisitos específicos impeçam você de fazer isso. A classe InputBase<TValue> é mantida ativamente pela equipe do ASP.NET Core, garantindo que ela permaneça atualizada com os recursos e alterações de estrutura mais recentes do Blazor.

Componente de entrada com base em InputBase<T>

O componente de exemplo a seguir:

  • Herdada de InputBase<TValue>. Os componentes que herdam de InputBase<TValue> devem ser usados em um formulário Blazor (EditForm).
  • Obtém a entrada booliana de uma caixa de seleção.
  • Define a cor da tela de fundo de seu contêiner <div> com base no estado da caixa de seleção, que ocorre quando o método AfterChange é executado após a associação (@bind:after).
  • É necessário para substituir o método TryParseValueFromString da classe base, mas não processa dados de entrada de cadeia de caracteres porque uma caixa de seleção não fornece dados de cadeia de caracteres. Exemplos de implementações de TryParseValueFromString para outros tipos de componentes de entrada que processam a entrada de strings estão disponíveis na fonte de referência ASP.NET Core.

Observação

Os links de documentação para a fonte de referência do .NET geralmente carregam o branch padrão do repositório, que representa o desenvolvimento atual da próxima versão do .NET. Para selecionar uma marca para uma versão específica, use a lista suspensa para Alternar branches ou marcas. Para saber mais, confira Como selecionar uma marca de versão do código-fonte do 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 usar o componente anterior no formulário de exemplo starship (Starship3.razor/Starship.cs), substitua o bloco <div> do campo de aprovação de engenharia por uma instância de componente EngineeringApprovalInputDerived vinculada à propriedade IsValidatedDesign do modelo:

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

Componente de entrada com controle total do desenvolvedor

O componente de exemplo a seguir:

  • Não herda de InputBase<TValue>. O componente assume o controle total do processamento de entrada, incluindo associação, retornos de chamada e validação. O componente pode ser usado dentro ou fora de um formulário Blazor (EditForm).
  • Obtém a entrada booliana de uma caixa de seleção.
  • Altera a cor da tela de fundo se a caixa de seleção estiver marcada.

O código no componente inclui:

  • A propriedade Value é usada com vinculação bidirecional para obter ou definir o valor da entrada. ValueChanged é o retorno de chamada que atualiza o valor associado.

  • Quando usado em um formulário Blazor:

    • O EditContext é um valor em cascata.
    • fieldCssClass estiliza o campo com base no resultado da validação EditContext.
    • ValueExpression é uma expressão (Expression<Func<T>>) atribuída pela estrutura que identifica o valor associado.
    • FieldIdentifier identifica exclusivamente um único campo que pode ser editado, geralmente correspondendo a uma propriedade de modelo. O identificador de campo é criado com a expressão que identifica o valor associado (ValueExpression).
  • No manipulador de eventos OnChange:

    • O valor da entrada da caixa de seleção é obtido de InputFileChangeEventArgs.
    • A cor da tela de fundo e a cor do texto do elemento de contêiner <div> são definidas.
    • EventCallback.InvokeAsync invoca o delegado associado à associação e envia uma notificação de evento aos consumidores de que o valor foi alterado.
    • Se o componente for usado em um EditForm (a propriedade EditContext não é null), EditContext.NotifyFieldChanged é chamado para disparar a validação.

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 o componente anterior no formulário de exemplo starship (Starship3.razor/Starship.cs), substitua o bloco <div> do campo de aprovação de engenharia por uma instância de componente EngineeringApprovalInputStandalone vinculada à propriedade IsValidatedDesign do modelo:

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

O componente EngineeringApprovalInputStandalone também é funcional fora de um EditForm:

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

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

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

Botões de opção

O exemplo nesta seção baseia-se no formulário Starfleet Starship Database (componente Starship3) da seção Formulário de exemplo deste artigo.

Adicione os seguintes enum tipos ao aplicativo. Crie um arquivo para mantê-los ou adicione-os ao arquivo 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 }
}

Torne a classe ComponentEnums acessível ao:

  • Modelo Starship em Starship.cs (por exemplo, using static ComponentEnums;).
  • Formulário Starfleet Starship Database (Starship3.razor) (por exemplo, @using static ComponentEnums).

Use componentes InputRadio<TValue> com o componente InputRadioGroup<TValue> para criar um grupo de botões de opção. No exemplo a seguir, as propriedades são adicionadas ao modelo Starship descrito na seção Formulário de exemplo do artigo 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;

Atualize o formulário Starfleet Starship Database (componente Starship3) da seção Formulário de exemplo do artigo Componentes de entrada. Adicione os componentes para produzir:

  • Um grupo de botões de opção para o fabricante do navio.
  • Um grupo de botões de opção aninhado para a cor do motor e do navio.

Observação

Os grupos de botões de opção aninhados não são frequentemente usados em formulários porque podem resultar em um layout de controles de formulário desorganizado que pode confundir os usuários. No entanto, há casos em que eles fazem sentido no design da interface do usuário, como no exemplo a seguir, que junta recomendações para duas entradas de usuário, o motor e cor do navio. Um motor e uma cor são exigidos pela validação do formulário. O layout do formulário usa InputRadioGroup<TValue>s aninhados para juntar recomendações de motor e cor. No entanto, o usuário pode combinar qualquer motor com qualquer cor para enviar o formulário.

Observação

Certifique-se de disponibilizar a classe ComponentEnums para o componente no seguinte exemplo:

@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>

Observação

Se Name for omitido, os componentes InputRadio<TValue> serão agrupados por seu ancestral mais recente.

Se você implementou a marcação Razor anterior no componente Starship3 da seção Formulário de exemplo do artigo Componentes de entrada, atualize o registro em log do 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);

Ao trabalhar com botões de opção em um formulário, a associação de dados será tratada de forma diferente de outros elementos porque os botões de opção são avaliados como um grupo. O valor de cada botão de opção é fixo, mas o valor do grupo de botões de opção é o valor do botão de opção selecionado. O exemplo a seguir mostra como:

  • Manipule a associação de dados para um grupo de botões de opção.
  • Dê suporte à validação usando um 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 obter mais informações sobre parâmetros de tipo genérico (@typeparam), confira os seguintes artigos:

Use o modelo de exemplo a seguir.

StarshipModel.cs:

using System.ComponentModel.DataAnnotations;

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

O seguinte componente RadioButtonExample usa o componente InputRadio anterior para obter e validar uma classificação do usuário:

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");
    }
}