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.

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]
    public 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]
    public 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
  • 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.

Por padrão, 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 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 aplicativo Web Blazor.

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]
    public 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]
    public 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.

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")]
    public Holodeck? Model1 { get; set; }

    [SupplyParameterFromForm(FormName = "Holodeck2")]
    public 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; }
}

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

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>

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]
    public 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);
    }
}

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.

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