Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 13 de noviembre de 2018
ASP.NET Core MVCEn el post anterior veíamos cómo personalizar los mensajes de error generados durante el proceso de binding, y cómo aprovechar las posibilidades que nos brinda el framework para introducir textos localizados en esos puntos.

Hoy seguiremos profundizando en este tema, pero esta vez nos centraremos en modificar los textos por defecto de las anotaciones de datos y hacer que simplemente decorando una propiedad con un atributo como Required consigamos obtener mensajes de error localizados y a nuestro gusto, sin necesidad de establecer el ErrorMessage de forma explícita y, en algunos casos, ni siquiera tener que indicar dicho atributo.

Personalizando los proveedores de metadatos de validación

Los proveedores de metadatos de validación son componentes que se registran durante la configuración de los servicios de MVC, y son invocados por el framework para obtener las anotaciones o atributos de validación de propiedades.

Estos componentes, que implementan IValidationMetadataProvider, tienen la siguiente pinta:
public class CustomValidationMetadataProvider : IValidationMetadataProvider
{
    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        // Configurar metadatos de validación aquí
    }
}
El método CreateValidationMetadata() es invocado una única vez para cada objeto, parámetro o propiedad (el resultado es cacheado) del que se desee obtener información de validación. A través del argumento context podremos conocer información sobre el elemento evaluado y establecer sus metadatos de validación.

Esto podemos usarlo, por ejemplo, para añadir una anotación [Required] preconfigurada a todas las propiedades de tipo valor (int, long, DateTime, etc.) que no hayan sido previamente decoradas con dicha anotación:
public class CustomValidationMetadataProvider : IValidationMetadataProvider
{
    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        // Lets add a [Required] annotation to all value types
        if (context.Key.ModelType.GetTypeInfo().IsValueType
            && !context.ValidationMetadata.ValidatorMetadata.OfType<RequiredAttribute>().Any())
        {
            context.ValidationMetadata.ValidatorMetadata.Add(
                new RequiredAttribute() { ErrorMessage = "Hey, este campo es obligatorio" }
            );
        }
    }
}
Fijaos que de esta forma tan sencilla estamos modificando el mensaje por defecto del framework para obligatoriedad implícita en este tipo de campos. Si quisiéramos aplicar los cambios, simplemente deberíamos añadir el siguiente código al ConfigureServices() de nuestra aplicación:
services.AddMvc(opt =>
{
    opt.ModelMetadataDetailsProviders.Add(new CustomValidationMetadataProvider());
});

Estableciendo mensajes de error por defecto para las anotaciones

Jugando con el ejemplo anterior un poco, podemos ver que establecer un mensaje por defecto para las validaciones más habituales no es nada complicado. En la colección _context.ValidationMetadata.ValidatorMetadata podemos encontrar las anotaciones de validación asociadas al elemento evaluado, por lo que simplemente tendremos que recorrerlas y establecer sus propiedades como más nos convenga:
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
    // First, lets add a [Required] annotation to all value types
    if (context.Key.ModelType.GetTypeInfo().IsValueType
        && !context.ValidationMetadata.ValidatorMetadata.OfType<RequiredAttribute>().Any())

    // Now, set the ErrorMessage for all validation attributes 
    var validationAttributes = context.ValidationMetadata.ValidatorMetadata.OfType<ValidationAttribute>();
    foreach (var validationAttribute in validationAttributes)
    {
        if(validationAttribute is StringLengthAttribute) 
        {
            validationAttribute.ErrorMessage = "Hey, este campo es obligatorio";
        }
        else if(validationAttribute is RangeAttribute) 
        {
            validationAttribute.ErrorMessage = "Introduce valores entre {1} y {2}";
        }
        [...] // Set ErrorMessage for other validations
    }
}    
Y obviamente, si le damos una vuelta de tuerca más podemos utilizar este mismo enfoque para establecer los mensajes de texto localizados, aunque esto nos costará un poco más de trabajo.

Localizando los mensajes de validación por defecto

Lo primero que vamos a hacer es crear un archivo RESX con los mensajes de validación al que, por ejemplo, llamamos SharedResources.Resx:

El archivo de recursos

Hay varias cosas a tener en cuenta:
  • La ubicación del archivo puede ser cualquiera. En nuestro caso, lo introduciremos en una carpeta llamada "Resources" en el raíz del proyecto.
     
  • En las propiedades del archivo archivo RESX debemos establecer su custom tool al valor "PublicResXFileCodeGenerator". Esto hará que se genere automáticamente una clase que facilita el acceso a los recursos localizados en el archivo {NombreDeTuResx}.Designer.cs; en nuestro ejemplo, el nombre de la clase es SharedResources.
     
  • Por convención, las claves de los recursos coincidirán con el nombre del atributo de validación, pero eliminando el sufijo "Attribute". Así, para la anotación [Required], el texto localizado lo encontraremos bajo la clave "Required" en el RESX, "Range" para la anotación [Range] y así con todos.
Obviamente, podemos añadir traducciones en archivos RESX adicionales, como SharedResources.en.resx, SharedResources.fr.resx, etc. En estos casos, no será necesario modificar su custom tool.
A continuación, vamos a hacer que el proveedor de metadatos de validación configure correctamente los mensajes por defecto atendiendo a las convenciones y acciones anteriores.

Como puede verse en el siguiente código, estamos modificando nuestro proveedor de metadatos para que reciba en el constructor el tipo de la clase asociada al archivo RESX donde hemos depositado nuestros mensajes de error, y usamos esta información para configurar el mensaje por defecto del validador:
public class CustomValidationMetadataProvider : IValidationMetadataProvider
{
    private readonly ResourceManager _resourceManager;
    private readonly Type _resourceType;

    public CustomValidationMetadataProvider(Type type)

    {
        _resourceType = type;
        _resourceManager = new ResourceManager(type.FullName, type.GetTypeInfo().Assembly);
    }

    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        if (context.Key.ModelType.GetTypeInfo().IsValueType
            && !context.ValidationMetadata.ValidatorMetadata.OfType<RequiredAttribute>().Any())
        {
            context.ValidationMetadata.ValidatorMetadata.Add(new RequiredAttribute());
        }

        var validationAttributes = context.ValidationMetadata
                                          .ValidatorMetadata.OfType<ValidationAttribute>()
        foreach (var validationAttribute in validationAttributes)
        {
            if (validationAttribute.ErrorMessageResourceName == null 
                && validationAttribute.ErrorMessageResourceType == null)
            {
                // By convention, the resource key will coincide with the attribute
                // name, removing the suffix "Attribute" when needed
                var resourceKey = validationAttribute.GetType().Name;
                if (resourceKey.EndsWith("Attribute"))
                {
                    resourceKey = resourceKey.Substring(0, resourceKey.Length - 9);
                }

                // Patch the "StringLength with minimum value" case
                if (validationAttribute is StringLengthAttribute stringLength 
                    && stringLength.MinimumLength > 0)
                {
                    resourceKey = "StringLengthIncludingMinimum";
                }

                // Setup the message if the key exists
                if (_resourceManager.GetString(resourceKey) != null)
                {
                    validationAttribute.ErrorMessage = null;
                    validationAttribute.ErrorMessageResourceType = _resourceType;
                    validationAttribute.ErrorMessageResourceName = resourceKey;
                }
            }
        }
    }
}
Hecho esto, ya sólo nos queda suministrar la clase apropiada durante el registro de servicios:
services.AddMvc(opt =>
{
    opt.ModelMetadataDetailsProviders.Add(
        new CustomValidationMetadataProvider(typeof(SharedResources)));
        // "SharedResource" is the class generated from the RESX file
})

Por último

Por si queréis ver todo esto en funcionamiento, o incluso copiar y pegar algo de código para vuestros proyectos, he dejado en Github una demo funcional de lo descrito en este artículo y el anterior, por lo que tenéis una solución con la localización y personalización de mensajes resuelta por completo :)

https://github.com/VariableNotFound/i18n-validations

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!