Compartir vía


Validación de formularios de Blazor Core de ASP.NET

Nota:

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

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulta la versión .NET 8 de este artículo.

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

Validación de formularios

En escenarios de validación básica de formularios, una instancia EditForm puede usar instancias EditContext y ValidationMessageStore declaradas para validar campos de formulario. Un controlador para el evento OnValidationRequested de EditContext ejecuta la lógica de validación personalizada. El resultado del controlador actualiza la instancia ValidationMessageStore.

La validación básica de formularios es útil en los casos en los que el modelo del formulario se define dentro del componente que hospeda el formulario, ya sea como miembros directamente en el componente o en una subclase. Se recomienda el uso de un componente de validador cuando se utiliza una clase de modelo independiente en varios componentes.

En Blazor Web App, la validación del lado cliente requiere un circuito activo BlazorSignalR. La validación del lado cliente no está disponible para formularios en componentes que han adoptado la representación estática del lado servidor (SSR estático). Los formularios que adoptan SSR estático se validan en el servidor después de enviar el formulario.

En el componente siguiente, el método de controlador HandleValidationRequested borra los mensajes de validación existentes llamando a ValidationMessageStore.Clear antes de validar el formulario.

Starship8.razor:

@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

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

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

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

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship8">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

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

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

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

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/starship-8"
@implements IDisposable
@inject ILogger<Starship8> Logger

<h2>Holodeck Configuration</h2>

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem1" />
            Safety Subsystem
        </label>
    </div>
    <div>
        <label>
            <InputCheckbox @bind-Value="Model!.Subsystem2" />
            Emergency Shutdown Subsystem
        </label>
    </div>
    <div>
        <ValidationMessage For="() => Model!.Options" />
    </div>
    <div>
        <button type="submit">Update</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    public Holodeck? Model { get; set; }

    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender,
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!Model!.Options)
        {
            messageStore?.Add(() => Model.Options, "Select at least one.");
        }
    }

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

    public class Holodeck
    {
        public bool Subsystem1 { get; set; }
        public bool Subsystem2 { get; set; }
        public bool Options => Subsystem1 || Subsystem2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}

Componente validador de anotaciones de datos y validación personalizada

El componente DataAnnotationsValidator expone la validación de anotaciones de datos en un EditContext en cascada. La habilitación de la validación de anotaciones de datos requiere el componente DataAnnotationsValidator. Para usar un sistema de validación distinto al de las anotaciones de datos, use una implementación personalizada del componente DataAnnotationsValidator. Las implementaciones del marco de DataAnnotationsValidator están disponibles para su inspección en el origen de referencia.

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

Blazor realiza dos tipos de validación:

  • La validación del campo se realiza cuando el usuario sale de un campo usando la tecla TAB. Durante una validación del campo, el componente DataAnnotationsValidator asocia al campo todos los resultados de validación notificados.
  • La validación del modelo se realiza cuando el usuario envía el formulario. Durante una validación del modelo, el componente DataAnnotationsValidator intenta determinar el campo en función del nombre del miembro que se indica en el resultado de la validación. Los resultados de validación que no están asociados a un miembro individual se asocian al modelo en lugar de a un campo.

Componentes de validador

Los componentes de validador admiten la validación de formularios mediante la administración de un objeto ValidationMessageStore para el objeto EditContext de un formulario.

El marco Blazor proporciona el componente DataAnnotationsValidator para adjuntar la compatibilidad con la validación a formularios en función de atributos de validación (anotaciones de datos). Puedes crear componentes de validador personalizados para procesar los mensajes de validación para diferentes formularios de la misma página o el mismo formulario en distintos pasos del procesamiento de formularios (por ejemplo, la validación del lado cliente seguida de la del lado servidor). El ejemplo de componente de validador que se muestra en esta sección, CustomValidation, se usa en las secciones siguientes de este artículo:

De los validadores integrados de anotaciones de datos, solo no se admite el atributo de validación [Remote] en Blazor.

Nota:

En muchos casos, se pueden usar atributos de validación de anotación de datos personalizados en lugar de componentes de validador personalizados. Los atributos personalizados que se aplican al modelo del formulario se activan con el uso del componente DataAnnotationsValidator. Cuando se usan con la validación del lado servidor, los atributos personalizados que se aplican al modelo deben ser ejecutables en el servidor. Para obtener más información, consulta la sección Atributos de validación personalizados.

Crea un componente de validador a partir de ComponentBase:

  • El objeto EditContext del formulario es un parámetro en cascada del componente.
  • Cuando se inicializa el componente de validador, se crea un objeto ValidationMessageStore para mantener una lista actual de errores del formulario.
  • El almacén de mensajes recibe errores cuando el código del desarrollador del componente del formulario llama al método DisplayErrors. Los errores se pasan al método DisplayErrors en un objeto Dictionary<string, List<string>>. En el diccionario, la clave es el nombre del campo de formulario que tiene uno o más errores. El valor es la lista de errores.
  • Los mensajes se borran cuando se produce alguno de los casos siguientes:
    • La validación se solicita en el objeto EditContext cuando se genera el evento OnValidationRequested. Todos los errores se borran.
    • Un campo cambia en el formulario cuando se genera el evento OnFieldChanged. Solo se borran los errores del campo.
    • El código del desarrollador llama al método ClearErrors. Todos los errores se borran.

Actualiza el espacio de nombres de la siguiente clase para que coincida con el espacio de nombres de la aplicación.

CustomValidation.cs:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

public class CustomValidation : ComponentBase
{
    private ValidationMessageStore? messageStore;

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

    protected override void OnInitialized()
    {
        if (CurrentEditContext is null)
        {
            throw new InvalidOperationException(
                $"{nameof(CustomValidation)} requires a cascading " +
                $"parameter of type {nameof(EditContext)}. " +
                $"For example, you can use {nameof(CustomValidation)} " +
                $"inside an {nameof(EditForm)}.");
        }

        messageStore = new(CurrentEditContext);

        CurrentEditContext.OnValidationRequested += (s, e) => 
            messageStore?.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore?.Clear(e.FieldIdentifier);
    }

    public void DisplayErrors(Dictionary<string, List<string>> errors)
    {
        if (CurrentEditContext is not null)
        {
            foreach (var err in errors)
            {
                messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
            }

            CurrentEditContext.NotifyValidationStateChanged();
        }
    }

    public void ClearErrors()
    {
        messageStore?.Clear();
        CurrentEditContext?.NotifyValidationStateChanged();
    }
}

Importante

Es necesario especificar un espacio de nombres al derivar de ComponentBase. Si no se especifica un espacio de nombres, se produce un error de compilación:

Tag helpers cannot target tag name '<global namespace>.{CLASS NAME}' because it contains a ' ' character.

El marcador de posición {CLASS NAME} es el nombre de la clase de componente. El ejemplo de validador personalizado de esta sección especifica el espacio de nombres de ejemplo BlazorSample.

Nota

Las expresiones lambda anónimas son controladores de eventos registrados para OnValidationRequested y OnFieldChanged en el ejemplo anterior. No es necesario implementar IDisposable y cancelar la suscripción de los delegados de eventos en este escenario. Para obtener más información, consulta Ciclo de vida de los componentes Razor de ASP.NET Core.

Validación de la lógica de negocios con un componente de validador

Para la validación de la lógica de negocios general, utiliza un componente de validador que recibe errores del formulario en un diccionario.

La validación básica es útil en los casos en los que el modelo del formulario se define dentro del componente que hospeda el formulario, ya sea como miembros directamente en el componente o en una subclase. Se recomienda el uso de un componente de validador cuando se utiliza una clase de modelo independiente en varios componentes.

En el ejemplo siguiente:

  • Se utiliza una versión abreviada del formulario Starfleet Starship Database (componente Starship3) de la sección Formulario de ejemplo del artículo Componentes de entrada que solo acepta la clasificación y descripción de la nave espacial. La validación de anotaciones de datos no se desencadena al enviar el formulario porque el componente DataAnnotationsValidator no está incluido en el formulario.
  • Se usa el componente CustomValidation de la sección Componentes de validador de este artículo.
  • La validación necesita un valor para la descripción del envío (Description) si el usuario selecciona la clasificación de envío ”Defense” (Classification).

Cuando los mensajes de validación se establecen en el componente, se agregan al componente ValidationMessageStore del validador y se muestran en el resumen de validación de EditForm.

Starship9.razor:

@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

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

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}
@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship9">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

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

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}
@page "/starship-9"
@inject ILogger<Starship9> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;

    public Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        customValidation?.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (Model!.Classification == "Defense" &&
                string.IsNullOrEmpty(Model.Description))
        {
            errors.Add(nameof(Model.Description),
                new() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("Submit called: Processing the form");
        }
    }
}

Nota:

Como alternativa al uso de componentes de validación, se pueden utilizar atributos de validación de anotación de datos. Los atributos personalizados que se aplican al modelo del formulario se activan con el uso del componente DataAnnotationsValidator. Cuando se usan con la validación del lado servidor, los atributos deben ser ejecutables en el servidor. Para obtener más información, consulta la sección Atributos de validación personalizados.

Validación del servidor con un componente de validador

Esta sección se centra en escenarios de Blazor Web App, pero el enfoque para cualquier tipo de aplicación que use la validación del servidor con la API web adopta el mismo enfoque general.

Esta sección se centra en escenarios de Blazor WebAssembly hospedados, pero el enfoque para cualquier tipo de aplicación que use la validación del servidor con la API web adopta el mismo enfoque general.

La validación del lado servidor se admite además de la validación del lado cliente:

  • Procesa la validación del lado cliente en el formulario con el componente DataAnnotationsValidator.
  • Cuando el formulario pasa la validación del lado cliente (se llama a OnValidSubmit), envía EditContext.Model a una API de servidor backend para el procesamiento del formulario.
  • Procesa la validación del modelo en el servidor.
  • La API del servidor incluye la validación de anotaciones de datos del marco integrada y la lógica de validación personalizada proporcionada por el desarrollador. Si la validación se supera en el servidor, procesa el formulario y devuelve un código de estado correcto (200 - OK). Si se produce un error en la validación, devuelve un código de estado de error (400 - Bad Request) y los errores de validación de campos.
  • Si es correcto, puedes deshabilitar el formulario, o bien mostrar los errores.

La validación básica es útil en los casos en los que el modelo del formulario se define dentro del componente que hospeda el formulario, ya sea como miembros directamente en el componente o en una subclase. Se recomienda el uso de un componente de validador cuando se utiliza una clase de modelo independiente en varios componentes.

El ejemplo siguiente se basa en:

Coloca el modelo Starship (Starship.cs) en un proyecto de biblioteca de clases compartida para que los proyectos de cliente y servidor puedan usar el modelo. Agrega o actualiza el espacio de nombres para que coincida con el espacio de nombres de la aplicación compartida (por ejemplo, namespace BlazorSample.Shared). Dado que el modelo requiere anotaciones de datos, confirma que la biblioteca de clases compartida usa el marco compartido o agrega el paquete de System.ComponentModel.Annotations al proyecto compartido.

Nota:

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulta los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (documentación de NuGet). Confirma las versiones correctas del paquete en NuGet.org.

En el proyecto principal de la Blazor Web App, agrega un controlador para procesar solicitudes de validación de nave estelar y devuelve mensajes de validación con errores. Actualiza los espacios de nombres de la última instrucción using para el proyecto de biblioteca de clases compartida y namespace para la clase de controlador. Además de la validación de anotaciones de datos del lado cliente y servidor, el controlador valida que se proporciona un valor para la descripción del envío (Description) si el usuario selecciona la clasificación de envío Defense (Classification).

Coloca el modelo Starship (Starship.cs) en el proyecto Shared de la solución para que las aplicaciones cliente y servidor puedan usarlo. Agrega o actualiza el espacio de nombres para que coincida con el espacio de nombres de la aplicación compartida (por ejemplo, namespace BlazorSample.Shared). Como en el modelo se necesitan anotaciones de datos, agrega el paquete System.ComponentModel.Annotations al proyecto Shared.

Nota:

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulta los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (documentación de NuGet). Confirma las versiones correctas del paquete en NuGet.org.

En el proyecto Server, agrega un controlador para procesar las solicitudes de validación de Starship y devuelve los mensajes de validación con errores. Actualiza los espacios de nombres de la última instrucción using para el proyecto Shared y namespace para la clase de controlador. Además de la validación de anotaciones de datos del lado cliente y servidor, el controlador valida que se proporciona un valor para la descripción del envío (Description) si el usuario selecciona la clasificación de envío Defense (Classification).

La validación de la clasificación de envío Defense solo se produce en el lado servidor del controlador porque el formulario siguiente no realiza la mismo validación del lado cliente cuando el formulario se envía al servidor. La validación del servidor sin validación de cliente es común en las aplicaciones que requieren la validación de lógica de negocios privada de la entrada de usuario en el servidor. Por ejemplo, la información privada de los datos almacenados para un usuario podría ser necesaria para validar la entrada del usuario. Obviamente, los datos privados no se pueden enviar al cliente para la validación del lado cliente.

Nota:

El controlador StarshipValidation de esta sección utiliza Microsoft Identity 2.0. La API web solo acepta tokens para los usuarios que tienen el ámbito ”API.Access" para esta API. Se requiere personalización adicional si el nombre de ámbito de la API es diferente de API.Access.

Para obtener más información acerca de la seguridad, consulta:

Controllers/StarshipValidation.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class StarshipValidationController(
    ILogger<StarshipValidationController> logger) 
    : ControllerBase
{
    static readonly string[] scopeRequiredByApi = new[] { "API.Access" };

    [HttpPost]
    public async Task<IActionResult> Post(Starship model)
    {
        HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

        try
        {
            if (model.Classification == "Defense" && 
                string.IsNullOrEmpty(model.Description))
            {
                ModelState.AddModelError(nameof(model.Description),
                    "For a 'Defense' ship " +
                    "classification, 'Description' is required.");
            }
            else
            {
                logger.LogInformation("Processing the form asynchronously");

                // async ...

                return Ok(ModelState);
            }
        }
        catch (Exception ex)
        {
            logger.LogError("Validation Error: {Message}", ex.Message);
        }

        return BadRequest(ModelState);
    }
}

Confirma o actualiza el espacio de nombres del controlador anterior (BlazorSample.Server.Controllers) para que coincida con el espacio de nombres de los controladores de la aplicación.

Cuando se produce un error de validación de enlace de modelos en el servidor, un objeto ApiController (ApiControllerAttribute) devuelve normalmente una respuesta de solicitud incorrecta predeterminada con un objeto ValidationProblemDetails. La respuesta contiene otros datos además de los errores de validación, como se muestra en el ejemplo siguiente cuando no se envían todos los campos del formulario Starfleet Starship Database y no supera la validación:

{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Id": ["The Id field is required."],
    "Classification": ["The Classification field is required."],
    "IsValidatedDesign": ["This form disallows unapproved ships."],
    "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
  }
}

Nota:

para mostrar la respuesta JSON anterior, debes deshabilitar la validación del lado cliente del formulario para permitir el envío de formularios de campo vacíos, o bien usar una herramienta para enviar una solicitud directamente a la API del servidor, como Firefox Browser Developer.

Si la API del servidor devuelve la respuesta JSON predeterminada anterior, el cliente puede analizarla en el código para desarrolladores para obtener los elementos secundarios del nodo errors para el procesamiento de los errores de validación de los formularios. No es conveniente escribir código para desarrolladores para analizar el archivo. El análisis manual de JSON requiere generar un Dictionary<string, List<string>> de errores después de llamar a ReadFromJsonAsync. Idealmente, la API de servidor solo debe devolver los errores de validación, como se muestra en el ejemplo siguiente:

{
  "Id": ["The Id field is required."],
  "Classification": ["The Classification field is required."],
  "IsValidatedDesign": ["This form disallows unapproved ships."],
  "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}

Para modificar la respuesta de la API de servidor a fin de que solo devuelva los errores de validación, cambia el delegado que se invoca en las acciones anotadas con ApiControllerAttribute en el archivo Program. Para el punto de conexión de API (/StarshipValidation), devuelve un objeto BadRequestObjectResult con el objeto ModelStateDictionary. Para cualquier otro punto de conexión de API, conserva el comportamiento predeterminado mediante la devolución el resultado del objeto con un ValidationProblemDetails nuevo.

Agrega el espacio de nombres Microsoft.AspNetCore.Mvc a la parte superior del archivo Program en el proyecto principal de la Blazor Web App:

using Microsoft.AspNetCore.Mvc;

En el archivo Program, agrega o actualiza el siguiente método de extensión AddControllersWithViews y agrega la siguiente llamada a ConfigureApiBehaviorOptions:

builder.Services.AddControllersWithViews()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            if (context.HttpContext.Request.Path == "/StarshipValidation")
            {
                return new BadRequestObjectResult(context.ModelState);
            }
            else
            {
                return new BadRequestObjectResult(
                    new ValidationProblemDetails(context.ModelState));
            }
        };
    });

Si vas a agregar controladores al proyecto principal de Blazor Web App por primera vez, asigna los puntos de conexión del controlador al colocar el código anterior que registra los servicios para los controladores. En el ejemplo siguiente se usan rutas de controlador predeterminadas:

app.MapDefaultControllerRoute();

Nota:

En el ejemplo anterior se registran explícitamente los servicios de controlador llamando a AddControllersWithViews para mitigar automáticamente los ataques de falsificación de solicitud entre sitios (XSRF/CSRF). Si simplemente usas AddControllers, la anti-falsificación no se habilita automáticamente.

Para obtener más información sobre las respuestas de error de enrutamiento y validación del controlador, consulta los siguientes recursos:

En el proyecto .Client, agrega el componente CustomValidation que se muestra en la sección Componentes de validador. Actualiza el espacio de nombres para que coincida con la aplicación (por ejemplo, namespace BlazorSample.Client).

En el proyecto .Client , el formulario Starfleet Starship Database se actualiza para mostrar los errores de validación del servidor con la ayuda del componente CustomValidation. Cuando la API de servidor devuelve mensajes de validación, se agregan al objeto ValidationMessageStoredel componente CustomValidation. Los errores están disponibles en el objeto EditContext del formulario para que los muestre el resumen de validación del formulario.

En el componente siguiente, actualiza el espacio de nombres del proyecto compartido (@using BlazorSample.Shared) al espacio de nombres del proyecto compartido. Ten en cuenta que el formulario requiere autorización, por lo que el usuario debe haber iniciado sesión en la aplicación para desplazarse al formulario.

Agrega el espacio de nombres Microsoft.AspNetCore.Mvc a la parte superior del archivo Program en la aplicación Server:

using Microsoft.AspNetCore.Mvc;

En el archivo Program, busca el método de extensión AddControllersWithViews y agrega la llamada siguiente a ConfigureApiBehaviorOptions:

builder.Services.AddControllersWithViews()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            if (context.HttpContext.Request.Path == "/StarshipValidation")
            {
                return new BadRequestObjectResult(context.ModelState);
            }
            else
            {
                return new BadRequestObjectResult(
                    new ValidationProblemDetails(context.ModelState));
            }
        };
    });

Nota:

En el ejemplo anterior se registran explícitamente los servicios de controlador llamando a AddControllersWithViews para mitigar automáticamente los ataques de falsificación de solicitud entre sitios (XSRF/CSRF). Si simplemente usas AddControllers, la anti-falsificación no se habilita automáticamente.

En el proyecto Client, agrega el componente CustomValidation que se muestra en la sección Componentes de validador. Actualiza el espacio de nombres para que coincida con la aplicación (por ejemplo, namespace BlazorSample.Client).

En el proyecto Client , el formulario Starfleet Starship Database se actualiza para mostrar los errores de validación del servidor con la ayuda del componente CustomValidation. Cuando la API de servidor devuelve mensajes de validación, se agregan al objeto ValidationMessageStoredel componente CustomValidation. Los errores están disponibles en el objeto EditContext del formulario para que los muestre el resumen de validación del formulario.

En el componente siguiente, actualiza el espacio de nombres del proyecto Shared (@using BlazorSample.Shared) al espacio de nombres del proyecto compartido. Ten en cuenta que el formulario requiere autorización, por lo que el usuario debe haber iniciado sesión en la aplicación para desplazarse al formulario.

Starship10.razor:

Nota:

Los formularios basados en EditForm habilitan automáticamente la compatibilidad contra la falsificación. El controlador debe usar AddControllersWithViews para registrar servicios de controlador y habilitar automáticamente la compatibilidad contra la falsificación para la API web.

@page "/starship-10"
@rendermode InteractiveWebAssembly
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm FormName="Starship10" Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="Model!.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Production Date:
            <InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@disabled">Submit</button>
    </div>
    <div style="@messageStyles">
        @message
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;
    private bool disabled;
    private string? message;
    private string messageStyles = "visibility:hidden";

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

    protected override void OnInitialized() => 
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private async Task Submit(EditContext editContext)
    {
        customValidation?.ClearErrors();

        try
        {
            var response = await Http.PostAsJsonAsync<Starship>(
                "StarshipValidation", (Starship)editContext.Model);

            var errors = await response.Content
                .ReadFromJsonAsync<Dictionary<string, List<string>>>() ?? 
                new Dictionary<string, List<string>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Any())
            {
                customValidation?.DisplayErrors(errors);
            }
            else if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Validation failed. Status Code: {response.StatusCode}");
            }
            else
            {
                disabled = true;
                messageStyles = "color:green";
                message = "The form has been processed.";
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
        catch (Exception ex)
        {
            Logger.LogError("Form processing error: {Message}", ex.Message);
            disabled = true;
            messageStyles = "color:red";
            message = "There was an error processing the form.";
        }
    }
}

El proyecto .Client de un objeto Blazor Web App también debe registrar un objeto HttpClient para las solicitudes HTTP POST a un controlador de API web de back-end. Confirma o agrega lo siguiente al archivo Program del proyecto de .Client:

builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

En el ejemplo anterior se establece la dirección base con builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress), que obtiene la dirección base de la aplicación y se deriva normalmente del valor href de la etiqueta <base> en la página host.

@page "/starship-10"
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<Starship10> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification" disabled="@disabled">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="Model!.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </div>
    <div>
        <label>
            Production Date:
            <InputDate @bind-Value="Model!.ProductionDate" disabled="@disabled" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@disabled">Submit</button>
    </div>
    <div style="@messageStyles">
        @message
    </div>
</EditForm>

@code {
    private CustomValidation? customValidation;
    private bool disabled;
    private string? message;
    private string messageStyles = "visibility:hidden";

    public Starship? Model { get; set; }

    protected override void OnInitialized() => 
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private async Task Submit(EditContext editContext)
    {
        customValidation?.ClearErrors();

        try
        {
            var response = await Http.PostAsJsonAsync<Starship>(
                "StarshipValidation", (Starship)editContext.Model);

            var errors = await response.Content
                .ReadFromJsonAsync<Dictionary<string, List<string>>>() ?? 
                new Dictionary<string, List<string>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Any())
            {
                customValidation?.DisplayErrors(errors);
            }
            else if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Validation failed. Status Code: {response.StatusCode}");
            }
            else
            {
                disabled = true;
                messageStyles = "color:green";
                message = "The form has been processed.";
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
        catch (Exception ex)
        {
            Logger.LogError("Form processing error: {Message}", ex.Message);
            disabled = true;
            messageStyles = "color:red";
            message = "There was an error processing the form.";
        }
    }
}

Nota:

Como alternativa al uso de un componente de validación, se pueden usar atributos de validación de anotación de datos. Los atributos personalizados que se aplican al modelo del formulario se activan con el uso del componente DataAnnotationsValidator. Cuando se usan con la validación del lado servidor, los atributos deben ser ejecutables en el servidor. Para obtener más información, consulta la sección Atributos de validación personalizados.

Nota:

El enfoque de validación del lado servidor de esta sección es adecuado para cualquiera de los ejemplos de soluciones Blazor WebAssembly hospedadas de este conjunto de documentación:

InputText basado en el evento de entrada

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

Resumen de validación y componentes de los mensajes de validación

El componente ValidationSummary, que resume todos los mensajes de validación, es similar a la aplicación auxiliar de etiquetas ValidationSummary:

<ValidationSummary />

Mensajes de validación de salida de un modelo específico con el parámetro Model:

<ValidationSummary Model="Model" />

El componente ValidationMessage<TValue>, que muestra los mensajes de validación de un campo específico, es similar a la aplicación auxiliar de etiquetas Validation Message. Especifica el campo de validación con el atributo For y una expresión lambda donde se indique la propiedad del modelo:

<ValidationMessage For="@(() => Model!.MaximumAccommodation)" />

Los componentes ValidationMessage<TValue> y ValidationSummary admiten atributos arbitrarios. Cualquier atributo que no coincida con un parámetro de componente se agrega a los elementos <div> o <ul> generados.

Controla el estilo de los mensajes de validación en la hoja de estilos de la aplicación (wwwroot/css/app.css o wwwroot/css/site.css). La clase validation-message predeterminada establece el color del texto de los mensajes de validación en rojo:

.validation-message {
    color: red;
}

Determinar si un campo de formulario es válido

Usa EditContext.IsValid para determinar si un campo es válido sin obtener mensajes de validación.

Compatible, pero no recomendado:

var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

Recomendado:

var isValid = editContext.IsValid(fieldIdentifier);

Atributos de validación personalizados

Para asegurarnos de que un resultado de validación está correctamente asociado a un campo cuando se usa un atributo de validación personalizado, pasa el elemento MemberName del contexto de validación al crear el elemento ValidationResult.

CustomValidator.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class CustomValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
        ValidationContext validationContext)
    {
        ...

        return new ValidationResult("Validation message to user.",
            new[] { validationContext.MemberName });
    }
}

Inserta servicios en atributos de validación personalizados a través de ValidationContext. En el ejemplo siguiente se muestra un formulario de un chef de ensaladas que valida la entrada del usuario con la inserción de dependencias (DI).

La clase SaladChef indica la lista de ingredientes de Starship aprobada para una ensalada Ten Forward.

SaladChef.cs:

namespace BlazorSample;

public class SaladChef
{
    public string[] SaladToppers = { "Horva", "Kanda Root", "Krintar", "Plomeek",
        "Syto Bean" };
}

Registra SaladChef en el contenedor de inserción de dependencias de la aplicación en el archivo Program:

builder.Services.AddTransient<SaladChef>();

El método IsValid de la siguiente clase SaladChefValidatorAttribute obtiene el servicio SaladChef de la inserción de dependencias para comprobar la entrada del usuario.

SaladChefValidatorAttribute.cs:

using System.ComponentModel.DataAnnotations;

namespace BlazorSample;

public class SaladChefValidatorAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value,
        ValidationContext validationContext)
    {
        var saladChef = validationContext.GetRequiredService<SaladChef>();

        if (saladChef.SaladToppers.Contains(value?.ToString()))
        {
            return ValidationResult.Success;
        }

        return new ValidationResult("Is that a Vulcan salad topper?! " +
            "The following toppers are available for a Ten Forward salad: " +
            string.Join(", ", saladChef.SaladToppers));
    }
}

El componente siguiente valida la entrada del usuario aplicando SaladChefValidatorAttribute ([SaladChefValidator]) a la cadena de ingredientes de ensaladas (SaladIngredient).

Starship12.razor:

@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off" FormName="Starship12">
    <DataAnnotationsValidator />
    <div>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() =>
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off" FormName="Starship12">
    <DataAnnotationsValidator />
    <div>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() =>
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}
@page "/starship-12"
@inject SaladChef SaladChef

<EditForm Model="this" autocomplete="off">
    <DataAnnotationsValidator />
    <p>
        <label>
            Salad topper (@saladToppers):
            <input @bind="SaladIngredient" />
        </label>
    </p>
    <button type="submit">Submit</button>
    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>
</EditForm>

@code {
    private string? saladToppers;

    [SaladChefValidator]
    public string? SaladIngredient { get; set; }

    protected override void OnInitialized() => 
        saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
}

Atributos de clase CSS de validación personalizados

Los atributos de clase CSS de validación personalizados son útiles al integrar con marcos de CSS, como Bootstrap.

Para especificar atributos de clase CSS de validación personalizados, empieza por proporcionar estilos CSS para la validación personalizada. En el ejemplo siguiente, se especifican estilos válidos (validField) y no válidos (invalidField).

Agrega la clase CSS siguiente a la hoja de estilos de la aplicación:

.validField {
    border-color: lawngreen;
}

.invalidField {
    background-color: tomato;
}

Crea una clase derivada de FieldCssClassProvider que compruebe los mensajes de validación de campos y aplica el estilo válido o no válido correspondiente.

CustomFieldClassProvider.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = editContext.IsValid(fieldIdentifier);

        return isValid ? "validField" : "invalidField";
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        return isValid ? "validField" : "invalidField";
    }
}

Establece la clase CustomFieldClassProvider como proveedor de clases CSS de campo en la instancia del formulario EditContext con SetFieldCssClassProvider.

Starship13.razor:

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

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

@code {
    private EditContext? editContext;

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

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

    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-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

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

@code {
    private EditContext? editContext;

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

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

    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-13"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship13> Logger

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

@code {
    private EditContext? editContext;

    public Starship? Model { get; set; }

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

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

En el ejemplo anterior se comprueba la validez de todos los campos del formulario y se aplica un estilo a cada uno. Si el formulario solo debe aplicar estilos personalizados a un subconjunto de campos, asegúrate de que CustomFieldClassProvider aplique estilos de forma condicional. En el ejemplo CustomFieldClassProvider2 siguiente solo se aplica un estilo al campo Name. Para los campos con nombres que no coinciden, se devuelve Namestring.Empty y no se aplica ningún estilo. Al usar reflexión, el campo se hace coincidir con la propiedad del nombre del modelo o el nombre del campo, no un id asignado a la entidad HTML.

CustomFieldClassProvider2.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = editContext.IsValid(fieldIdentifier);

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}

Nota

La asociación del nombre del campo en el ejemplo anterior distingue mayúsculas de minúsculas, por lo que un miembro de propiedad del modelo designado "Name" debe coincidir con una comprobación condicional en "Name":

  • Coincide correctamente: fieldId.FieldName == "Name"
  • No coincide: fieldId.FieldName == "name"
  • No coincide: fieldId.FieldName == "NAME"
  • No coincide: fieldId.FieldName == "nAmE"

Agrega una propiedad adicional a Model, por ejemplo:

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

Agrega una Description al formulario del componente CustomValidationForm:

<InputText @bind-Value="Model!.Description" />

Actualiza la instancia de EditContext del método OnInitialized del componente para usar el nuevo proveedor de clases CSS de campo:

editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());

Dado que una clase de validación CSS no se aplica al campo Description, no tiene estilo. Sin embargo, la validación de campos se ejecuta normalmente. Si se proporcionan más de diez caracteres, el resumen de validación indica el error:

La descripción es demasiado larga

En el ejemplo siguiente:

  • El estilo CSS personalizado se aplica al campo Name.

  • Cualquier otro campo aplica una lógica similar a la lógica predeterminada de Blazor y con los estilos de validación CSS de campo predeterminados de Blazor, modified con valid o invalid. Ten en cuenta que, para los estilos predeterminados, no es necesario agregarlos a la hoja de estilos de la aplicación si esta se basa en una plantilla de proyecto Blazor. En el caso de las aplicaciones que no se basan en una plantilla de proyecto Blazor, los estilos predeterminados se pueden agregar a la hoja de estilos de la aplicación:

    .valid.modified:not([type=checkbox]) {
        outline: 1px solid #26b050;
    }
    
    .invalid {
        outline: 1px solid red;
    }
    

CustomFieldClassProvider3.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider3 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = editContext.IsValid(fieldIdentifier);

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider3 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}

Actualiza la instancia de EditContext del método OnInitialized del componente para usar el proveedor de clases CSS del campo anterior:

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

Usar CustomFieldClassProvider3:

  • El campo Name usa los estilos CSS de validación personalizados de la aplicación.
  • El campo Description usa una lógica similar a la lógica de Blazor y los estilos de validación CSS del campo predeterminado de Blazor.

Validación de nivel de clase con IValidatableObject

La validación de nivel de clase con IValidatableObject (documentación de API) se admite para los modelos de formulario Blazor. La validación de IValidatableObject solo se ejecuta cuando se envía el formulario y solo si las demás validaciones se realizan correctamente.

Paquete de validación de anotaciones de datos de Blazor

Microsoft.AspNetCore.Components.DataAnnotations.Validation es un paquete que llena los huecos de experiencia de validación mediante el componente DataAnnotationsValidator. Actualmente, el paquete está en fase experimental.

Advertencia

En NuGet.org hay una versión candidata para lanzamiento más reciente del paquete Microsoft.AspNetCore.Components.DataAnnotations.Validation. Por ahora, sigue usando el paquete de versión candidata para lanzamiento experimental. Las características experimentales se proporcionan con el fin de explorar la viabilidad de las características y es posible que no se incluyan en una versión estable. Consulta los anuncios del repositorio de GitHub, el dotnet/aspnetcore repositorio de GitHub, o bien la sección de este tema para obtener más actualizaciones.

Atributo [CompareProperty]

CompareAttribute no funciona bien con el componente DataAnnotationsValidator porque DataAnnotationsValidator no asocia el resultado de la validación a un miembro específico. Esto puede generar un comportamiento incoherente entre la validación de nivel de campo y el momento en que el modelo completo se valida al enviarse. El paquete experimental Microsoft.AspNetCore.Components.DataAnnotations.Validation incluye un atributo de validación más, ComparePropertyAttribute, que pone fin a estas limitaciones. En una aplicación Blazor, [CompareProperty] es un reemplazo directo del atributo [Compare].

Tipos de colección, tipos complejos y modelos anidados

Blazor proporciona compatibilidad para validar la entrada del formulario mediante anotaciones de datos con el elemento integrado DataAnnotationsValidator, Sin embargo, DataAnnotationsValidator solo valida las propiedades de nivel superior del modelo enlazadas al formulario que no son propiedades de tipos complejos o de colección.

Para validar el gráfico de objetos completo del modelo enlazado, incluidas las propiedades de tipos complejos o de colección, usa el elemento ObjectGraphDataAnnotationsValidator proporcionado por el paquete experimentalMicrosoft.AspNetCore.Components.DataAnnotations.Validation:

<EditForm ...>
    <ObjectGraphDataAnnotationsValidator />
    ...
</EditForm>

Anota las propiedades del modelo con [ValidateComplexType]. En las siguientes clases de modelo, la clase ShipDescription contiene más anotaciones de datos para validar cuando el modelo está enlazado al formulario:

Starship.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    ...

    [ValidateComplexType]
    public ShipDescription ShipDescription { get; set; } = new();

    ...
}

ShipDescription.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class ShipDescription
{
    [Required]
    [StringLength(40, ErrorMessage = "Description too long (40 char).")]
    public string? ShortDescription { get; set; }

    [Required]
    [StringLength(240, ErrorMessage = "Description too long (240 char).")]
    public string? LongDescription { get; set; }
}

Habilitar el botón Enviar según la validación del formulario

Para habilitar y deshabilitar el botón Enviar según la validación del formulario, en el ejemplo siguiente:

  • Usa una versión abreviada del formulario Starfleet Starship Database anterior (componente Starship3) de la sección Formulario de ejemplo del artículo Componentes de entrada que solo acepta un valor para la identificación del barco. Las demás propiedades Starship reciben valores predeterminados válidos cuando se crea una instancia del tipo Starship.
  • Se usa el elemento EditContext del formulario para asignar el modelo cuando el componente se inicialice.
  • Se valida el formulario en la devolución de llamada OnFieldChanged del contexto para habilitar y deshabilitar el botón Enviar.
  • Se implementa IDisposable y se cancela la suscripción del controlador de eventos en el método Dispose. Para obtener más información, consulta Ciclo de vida de los componentes Razor de ASP.NET Core.

Nota:

Al asignar a EditForm.EditContext, no asignes también un EditForm.Model al componente EditForm.

Starship14.razor:

@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship14">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

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

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship14">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

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

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/starship-14"
@implements IDisposable
@inject ILogger<Starship14> Logger

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit" disabled="@formInvalid">Submit</button>
    </div>
</EditForm>

@code {
    private bool formInvalid = false;
    private EditContext? editContext;

    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??=
            new()
            {
                Id = "NCC-1701",
                Classification = "Exploration",
                MaximumAccommodation = 150,
                IsValidatedDesign = true,
                ProductionDate = new DateTime(2245, 4, 11)
            };
        editContext = new(Model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (editContext is not null)
        {
            formInvalid = !editContext.Validate();
            StateHasChanged();
        }
    }

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

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}

Si un formulario no está cargado previamente con valores válidos y deseas deshabilitar el botón Submit al cargar el formulario, establece formInvalid en true.

Un efecto secundario del método anterior es que un resumen de validación (componente ValidationSummary) se rellena con campos no válidos después de que el usuario interactúe con algún campo. Aborda este escenario de cualquiera de las maneras siguientes:

  • No uses un componente ValidationSummary en el formulario.
  • Haz que el componente ValidationSummary esté visible cuando se seleccione el botón Enviar (por ejemplo, en un método Submit).
<EditForm ... EditContext="editContext" OnValidSubmit="Submit" ...>
    <DataAnnotationsValidator />
    <ValidationSummary style="@displaySummary" />

    ...

    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    private string displaySummary = "display:none";

    ...

    private void Submit()
    {
        displaySummary = "display:block";
    }
}