blazor-validacion-personalizada-reglas-complejas

Validación personalizada en Blazor

  • 4 min

La validación personalizada permite aplicar reglas que no cubren los atributos estándar de DataAnnotations.

Estas reglas aparecen, por ejemplo, en situaciones como estas:

  • “La fecha de fin debe ser posterior a la de inicio”.
  • “Si el usuario es ‘Admin’, el teléfono es obligatorio”.
  • “El código de cupón debe existir en la base de datos”.

En Blazor podemos resolverlas con atributos propios, validación del modelo o mensajes añadidos al EditContext.

Atributos de validación personalizados

Esta es la opción más elegante si quieres reutilizar una regla en varios modelos. Consiste en crear tu propio atributo heredando de ValidationAttribute.

Supongamos que necesitamos validar que un booleano sea true (el clásico «Acepto los términos y condiciones»). [Required] no sirve porque false es un valor válido para bool.

Vamos a crear el atributo [MustBeTrue].

using System.ComponentModel.DataAnnotations;

public class MustBeTrueAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        // Si el valor es bool y es true, todo correcto
        if (value is bool booleano && booleano)
        {
            return ValidationResult.Success;
        }

        // Si falla, devolvemos el error (usamos el mensaje por defecto o uno genérico)
        return new ValidationResult(
            ErrorMessage ?? "Debes aceptar esta condición",
            [validationContext.MemberName!]);
    }
}
Copied!

Ahora podemos usarlo en nuestro modelo tan fácilmente como los estándar:

public class RegistroModel
{
    [MustBeTrue(ErrorMessage = "Debes aceptar los términos para continuar")]
    public bool AceptaTerminos { get; set; }
}
Copied!

Validación cruzada con IValidatableObject

Los atributos tienen una limitación: solo ven su propia propiedad. No saben nada de las demás.

¿Cómo validamos que “Contraseña” y “Confirmar Contraseña” son iguales? O que FechaFin > FechaInicio?

Para esto, el modelo puede implementar IValidatableObject. Su método Validate recibe el contexto completo y se ejecuta al enviar el formulario, después de que las validaciones de propiedades y de tipo hayan terminado sin errores.

public class ReservaModel : IValidatableObject
{
    [Required]
    public DateTime FechaEntrada { get; set; }

    [Required]
    public DateTime FechaSalida { get; set; }

    // Este método se ejecuta al final
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (FechaSalida <= FechaEntrada)
        {
            // Yield return permite devolver múltiples errores
            yield return new ValidationResult(
                "La fecha de salida debe ser posterior a la entrada",
                new[] { nameof(FechaSalida) }); // Indicamos qué campo iluminar en rojo
        }
    }
}
Copied!

Fíjate en el segundo parámetro de ValidationResult: new[] { nameof(FechaSalida) }. Esto es crucial. Le dice a Blazor: “Oye, aunque este error es de lógica general, quiero que pintes de rojo el input de FechaSalida y muestres el mensaje en su <ValidationMessage>”*

Validación manual con ValidationMessageStore

Algunas reglas dependen del servidor. Ejemplo clásico: El usuario rellena el formulario correctamente, pulsamos “Enviar”, y la base de datos nos dice: “El nombre de usuario ya existe”.

Ese error no podemos detectarlo antes de enviar. Necesitamos una forma de inyectar errores en el formulario manualmente después de una respuesta asíncrona.

Para esto usamos el ValidationMessageStore.

@implements IDisposable

<EditForm EditContext="editContext" OnSubmit="HandleSubmit" FormName="crear-usuario">
    <DataAnnotationsValidator />

    <label>Usuario:</label>
    <InputText @bind-Value="modelo.UserName" class="form-control" />
    <ValidationMessage For="@(() => modelo.UserName)" />

    <button type="submit">Crear</button>
</EditForm>

@code {
    private readonly UserModel modelo = new();
    private EditContext editContext = default!;
    private ValidationMessageStore messageStore = default!;

    protected override void OnInitialized()
    {
        // 1. Instanciamos el EditContext manualmente
        editContext = new(modelo);

        // 2. Creamos el almacén de mensajes vinculado a ese contexto
        messageStore = new(editContext);

        // 3. Limpiamos errores antiguos cuando el usuario modifica un campo
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        messageStore.Clear(e.FieldIdentifier);
        editContext.NotifyValidationStateChanged();
    }

    private async Task HandleSubmit()
    {
        // 4. Quitamos errores remotos anteriores y validamos DataAnnotations
        messageStore.Clear();

        if (editContext.Validate())
        {
            // Simulamos llamada al servidor
            var existe = await ServicioUsuarios.ExisteUsuario(modelo.UserName);

            if (existe)
            {
                // 5. INYECTAMOS EL ERROR MANUALMENTE
                messageStore.Add(editContext.Field(nameof(modelo.UserName)),
                    "Este usuario ya existe");

                // 6. Avisamos a la UI para que refresque los mensajes
                editContext.NotifyValidationStateChanged();
            }
            else
            {
                // Guardar...
            }
        }
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= HandleFieldChanged;
    }
}
Copied!

Este enfoque exige gestionar el EditContext y sus suscripciones, pero permite integrar errores devueltos por una API en los mismos componentes de validación.

Observa el evento OnFieldChanged. Es importante limpiar los errores manuales cuando el usuario empieza a escribir de nuevo. Si no, el mensaje “Usuario ya existe” se quedaría ahí para siempre, aunque el usuario cambie el nombre.

Cuándo usar cada estrategia

Para que no te pierdas, aquí tienes la guía de decisión:

EscenarioEstrategia recomendada
Regla simple (Email, Rango, Obligatorio)DataAnnotations Estándar ([Required])
Regla de negocio reutilizable (DNI, Tarjeta Crédito)Atributo Personalizado (ValidationAttribute)
Comparar dos campos (Fechas, Passwords)IValidatableObject
Errores que vienen de API/BackendValidationMessageStore