Vinculação de formulários no ASP.NET Core Blazor

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão do .NET 10 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, consulte a Política de Suporte do .NET e do .NET Core. Para a versão atual, consulte a versão do .NET 10 deste artigo.

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

EditForm / EditContext modelo

Um EditForm cria um EditContext com base no objeto atribuído 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.

Vinculação de modelos

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#.

Quando uma instância de objeto diferente é atribuída ao Model parâmetro de EditForm, o formulário cria um novo EditContext. O novo contexto começa sem campos modificados ou mensagens de validação.

Vinculaçã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 EditContextou 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
  • Enumerações

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.

Ao associar um tipo com parâmetros de construtor, se um parâmetro de construtor corresponder a uma propriedade por nome, o parâmetro de construtor terá precedência. O mapeador usa o DataMember.Name explícito da propriedade, se houver, como nome do campo do formulário, mas, caso contrário, ignora os atributos de mapeamento da propriedade. Os parâmetros do construtor são sempre obrigatórios.

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 dos formulários

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 para todos os formulários enviados por componentes renderizados estaticamente no servidor.
  • 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 atribuir um nome exclusivo a cada formulário para evitar erros, em tempo de execução, no envio do formulário caso a interatividade seja removida de 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 os dois formulários neste exemplo tenham o nome de formulário (Hello), os nomes de formulário não entram em conflito, e os eventos são direcionados para o formulário correto nos eventos POST do 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. Elementos de entrada baseados em InputBase<TValue> geram nomes de valores de formulário que correspondem aos nomes que Blazor usa para vinculaçã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.

Mapeamento de formulário Blazor com [SupplyParameterFromForm] não usa a vinculação de modelo do MVC. Atributos no Microsoft.AspNetCore.Mvc.ModelBinding namespace, como [BindNever] e [BindRequired], não têm suporte. Não use esses atributos para evitar superpostagem. Em vez disso, use um modelo de formulário dedicado, um modelo de exibição ou um objeto de transferência de dados (DTO) que inclua apenas as propriedades que os usuários têm permissão para modificar. Para obter mais informações, consulte Como atenuar ataques de superpostagem.

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 associar o parâmetro ao formulário com base no nome do formulário, para decidir se o valor precisa ser vinculado ou não.

O exemplo a seguir vincula independentemente dois formulários aos seus modelos pelo nome do 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 os detalhes do navio, vinculado 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

O framework instancia e preenche o FormMappingContext de um formulário, que é o contexto associado à operação de mapeamento de um formulário específico. 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.

InputText com base no evento de entrada

Use o componente InputText para criar um componente personalizado que usa o evento oninput (input) em vez do evento onchange (change). O uso do evento input dispara validação de campo em cada pressionamento de tecla.

O componente CustomInputText a seguir herda o componente InputText estrutura e define a associação de eventos ao evento oninput (input).

CustomInputText.razor:

@inherits InputText

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

O componente CustomInputText pode ser usado em qualquer lugar em que InputText é usado. O componente a seguir usa o componente CustomInputText compartilhado.

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

  • Herda 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" />

Quando o componente que herda de InputBase<TValue> for renderizado de forma estática, atribua a propriedade InputBase<TValue>.NameAttributeValue ao atributo name dos elementos <input>.

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

A atribuição anterior não será necessária se o componente tiver a garantia de sempre ser renderizado interativamente.

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 do componente inclui:

  • A propriedade Value é usada com vinculação bidirecional para obter ou definir o valor da entrada. ValueChanged é a função de callback que atualiza o valor vinculado.

  • 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 à vinculação e dispara uma notificação de evento para informar aos consumidores 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 os componentes InputRadio<TValue> com o componente InputRadioGroup<TValue> para criar um grupo de botões de rádio. 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 isso faz sentido no design de interface do usuário, como no exemplo a seguir, que combina recomendações para duas entradas do usuário: motor do navio 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:

  • Gerencie a vinculação de dados para um grupo de botões de rádio.
  • 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");
    }
}