martes, 13 de noviembre de 2018
En 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
Estos componentes, que implementan
Esto podemos usarlo, por ejemplo, para añadir una anotación
Hay varias cosas a tener en cuenta:
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:
https://github.com/VariableNotFound/i18n-validations
Publicado en Variable not found.
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, llamamosSharedResources.Resx
: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 esSharedResources
.
-
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, comoA 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.SharedResources.en.resx
,SharedResources.fr.resx
, etc. En estos casos, no será necesario modificar su custom tool.
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!
Enviar un nuevo comentario