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!]);
}
}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; }
}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
}
}
}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;
}
}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:
| Escenario | Estrategia 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/Backend | ValidationMessageStore |