Ir al contenido principal

Generación de Metadatos (Display, Required, RegularExpression y StringLenght) Desde Base de Datos

El namespace System.ComponentModel.DataAnnotations proporciona un conjunto de clases de metadatos para definir la forma en que se generan los controles HTML de formularios ASP.NET y ASP.NET MVC con muchas características bastante interesantes de validación y de etiquetas de controles, entre otros.

Es una forma muy poderosa por ejemplo de definir la etiqueta a mostrar, si un campo es o no requerido, asignar una expresión regular a cumplir para llevar a cabo las validaciones de propiedades, longitudes mínimas y máximas, una dirección de correo electrónico y más...

Pero como siempre existen escenarios en los cuáles dicha forma de generación de metadatos no cumplen con las espectativas, necesidades o requerimientos de alguno de nuestros clientes, o simplemente necesitamos extender la funcionalidad proporcionada por el .NET Framework y es en estos casos en los que la plataforma nos permite personalizar la funcionalidad predeterminada y cumplir con las espectativas necesarias.

Por ejemplo: ¿Qué pasa si requerimos generar los metadatos (controles de etiquetas y de captura) usando para ello la configuración específicada en base de datos?

Pues este es el problema que resolveremos en el siguiente Post.

Como es costumbre, veremos un video con una explicación somera de la forma en como solucionamos el problema:


Dentro de la Solución de Ejemplo, generaremos una carpeta llamada Custom en donde agregaremos las clases personalizadas de metadatos.

MetadataAttribute

Clase para la generación de cualquier attributo personalizado que será agregado a los metadatos en una clase ViewModel
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/// <summary>
/// Clase abstracta usada para definir atributos personalizados
/// </summary>
public abstract class MetadataAttribute : Attribute
{
    #region Métodos

    /// <summary>
    /// Método abstracto que se ejecuta para añadir propiedades de metadatos personalizados
    /// </summary>
    /// <param name="modelMetaData">Modelo de metadatos al que se adicionarán las propiedades personalizadas</param>
    public abstract void Process(ModelMetadata modelMetaData);

    #endregion
}

FieldNameAttribute

Atributo personalizado para la generación de metadatos configurados en un origen externo, (para nuestro ejemplo un archivo XML).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/// <summary>
/// Clase abstracta usada para definir atributos personalizados
/// </summary>
public abstract class MetadataAttribute : Attribute
{
    #region Métodos

    /// <summary>
    /// Método abstracto que se ejecuta para añadir propiedades de metadatos personalizados
    /// </summary>
    /// <param name="modelMetaData">Modelo de metadatos al que se adicionarán las propiedades personalizadas</param>
    public abstract void Process(ModelMetadata modelMetaData);

    #endregion
}

DatosPersonalesViewModel

ViewModel de prueba (en este caso se encuentra dentro de la carpeta Models, que es donde debería estar para aplicar el atributo personalizado FieldName).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class DatosPersonalesViewModel
{
    #region Propiedades

    [FieldName("Nombres")]
    public string Nombres { get; set; }

    [FieldName("ApellidoPaterno")]
    public string ApellidoPaterno { get; set; }

    [FieldName("ApellidoMaterno")]
    public string ApellidoMaterno { get; set; }

    [FieldName("FechaNacimiento")]
    public string FechaNacimiento { get; set; }

    [FieldName("CorreoElectronico")]
    public string CorreoElectrónico { get; set; }

    #endregion
}


CustomModelMetadataProvider

Se genera un proveedor de metadatos personalizado para ejecutar el método: Process de la clase MetadataAttribute para el agregado de los atributos personalizados definidos (para nuestro ejemplo: FieldNameAttribute) y se hace la asignación de la propiedad DisplayName para mostrar el texto adecuado usando el helper: Html.LabelFor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// <summary>
/// Proveedor de metadatos personalizado
/// </summary>
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    #region Sobrecarga de Métodos

    /// <summary>
    /// Asigna la propiedad Display desde base de datos
    /// </summary>
    /// <param name="attributes">Conjunto de atributos del metadato</param>
    /// <param name="containerType">Tipo de contenedor</param>
    /// <param name="modelAccessor">Objeto que permite el acceso al modelo</param>
    /// <param name="modelType">Tipo de datos del modelo</param>
    /// <param name="propertyName">Nombre de la propiedad con la que se está trabajando</param>
    /// <returns>Conjunto de metadatos generado</returns>
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes,
        Type containerType, Func<object> modelAccessor, Type modelType,
        string propertyName)
    {
        var metadata = base.CreateMetadata(attributes, containerType, modelAccessor,
            modelType, propertyName);
        // Se ejecuta el método process en atributos personalizados basados en la clase 
        // abstracta MetadataAttribute
        attributes.OfType<MetadataAttribute>().ToList().ForEach(x => x.Process(metadata));
        // Se asigna la propiedad DisplayName desde base de datos
        KeyValuePair<string, object> fieldName = metadata.AdditionalValues.
            SingleOrDefault(v => v.Key.Equals("FieldName"));
        if (!fieldName.Equals(new KeyValuePair<string, object>()))
        {
            XDocument datos = XDocument.Load(HostingEnvironment.MapPath("~/App_Data/Metadatos.xml"));
            XElement campo = datos.Root.Elements().
                Where(e => e.Attribute("nombre").Value.Equals(fieldName.Value.ToString())).
                FirstOrDefault();
            if (campo != null)
            {
                metadata.DisplayName = campo.Attribute("etiqueta").Value;
            }
        }
        return metadata;
    }

    #endregion
}

CustomHtmlHelpers

Se crea un helper personalizado llamado Html.TextBoxMaxLenghtFor basado en Html.TextBoxFor para la asignación de longitud máxima en campos de texto.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public static class CustomHtmlHelpers
{
    #region Html Helpers

    /// <summary>
    /// Html helper que nos permite asignar desde base de datos 
    /// la longitud máxima del campo
    /// </summary>
    /// <typeparam name="TModel">Tipo de datos del modelo</typeparam>
    /// <typeparam name="TValue">Valor usado para tener acceso al modelo</typeparam>
    /// <param name="helper">Html helper que nos permite tener acceso al modelo</param>
    /// <param name="expression">
    /// Expresión lambda que nos permite acceder a una propiedad del modelo
    /// </param>
    /// <returns>Control html a ser renderizado</returns>
    public static MvcHtmlString TextBoxMaxLengthFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
    {
        IDictionary<string, object> attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(new object());
        var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
        // Se ontiene el atributo FieldName (si existe)
        KeyValuePair<string, object> fieldName = metadata.AdditionalValues.SingleOrDefault(v => v.Key.Equals("FieldName"));
        if (!fieldName.Equals(new KeyValuePair<string, object>()))
        {
            // Se obtiene los metadatos desde el archivo XML (recuerda que puedes sustituirlo por una base de datos)
            XDocument datos = XDocument.Load(HostingEnvironment.MapPath("~/App_Data/Metadatos.xml"));
            XElement campo = datos.Root.Elements().
                Where(e => e.Attribute("nombre").Value.Equals(fieldName.Value.ToString())).
                FirstOrDefault();
            // Se añade el atributo HTML maxlength
            if (campo != null)
            {
                attributes.Add("maxlength", campo.Attribute("longitud").Value);
            }
        }
        // Se genera el control usando como base el helper TextBoxFor
        return InputExtensions.TextBoxFor(helper, expression, attributes);
    }

    /// <summary>
    /// Html helper que nos permite asignar desde base de datos 
    /// la longitud máxima del campo
    /// </summary>
    /// <typeparam name="TModel">Tipo de datos del modelo</typeparam>
    /// <typeparam name="TValue">Valor usado para tener acceso al modelo</typeparam>
    /// <param name="helper">Html helper que nos permite tener acceso al modelo</param>
    /// <param name="expression">Expresión lambda que nos permite acceder a una propiedad del modelo</param>
    /// <param name="htmlAttributes">Colección de atributos a adicionar al control HTML</param>
    /// <returns>Control html a ser renderizado</returns>
    public static MvcHtmlString TextBoxMaxLengthFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, object htmlAttributes)
    {
        // Se obtiene el objeto attributes generado desde la vista
        IDictionary<string, object> attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
        // Se obtiene el atributo personalizado FieldName (si existe)
        var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
        KeyValuePair<string, object> fieldName = metadata.AdditionalValues.SingleOrDefault(v => v.Key.Equals("FieldName"));
        if (!fieldName.Equals(new KeyValuePair<string, object>()))
        {
            // Se leen los metadatos desde el archivo XML (puede ser una base de datos)
            XDocument datos = XDocument.Load(HostingEnvironment.MapPath("~/App_Data/Metadatos.xml"));
            XElement campo = datos.Root.Elements().
                Where(e => e.Attribute("nombre").Value.Equals(fieldName.Value.ToString())).
                FirstOrDefault();
            if (campo != null)
            {
                // Se adiciona el atributo HTML maxlenght
                attributes.Add("maxlength", campo.Attribute("longitud").Value);
            }
        }
        // Se genera el control HTML usando como base el helper TextBoxFor
        return InputExtensions.TextBoxFor(helper, expression, attributes);
    }

    #endregion
}

CustomMetadataValidatorProvider

Proveedor personalizado de validación de metadatos para la configuración desde un origen externo de los atributos requeridos y de expresiones regulares.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/// <summary>
/// Proveedor de validación de metadatos personalizado
/// </summary>
public class CustomMetadataValidationProvider : DataAnnotationsModelValidatorProvider
{
    /// <summary>
    /// Obtiene los validadores desde base de datos
    /// </summary>
    /// <param name="metadata">Metadato de la propiedad a trabajar</param>
    /// <param name="context">Contexto del controlador que hace la petición</param>
    /// <param name="attributes">Conjunto de atributos agregados y por agregar</param>
    /// <returns>Validadores agregados a la propiedad</returns>
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {
        attributes = new List<Attribute>();
        // Se obtiene la propiedad FieldName desde metadatos (si existe)
        KeyValuePair<string, object> fieldName = metadata.AdditionalValues.SingleOrDefault(v => v.Key.Equals("FieldName"));
        if (!fieldName.Equals(new KeyValuePair<string, object>()))
        {
            // Se obtienen los metadatos del archivo XML
            XDocument datos = XDocument.Load(HostingEnvironment.MapPath("~/App_Data/Metadatos.xml"));
            XElement campo = datos.Root.Elements().
                Where(e => e.Attribute("nombre").Value.Equals(fieldName.Value.ToString())).
                FirstOrDefault();
            if (campo != null)
            {
                // Se adiciona el atributo de requerido
                if (bool.Parse(campo.Attribute("obligatorio").Value))
                {
                    RequiredAttribute required = new RequiredAttribute();
                    required.ErrorMessage = "El campo {0} es requerido";
                    (attributes as List<Attribute>).Add(required);
                }
                // Se adiciona el atributo de expresión regular junto con su mensaje de validación personalizado
                if (!string.IsNullOrEmpty(campo.Attribute("regex").Value))
                {
                    RegularExpressionAttribute regex =
                        new RegularExpressionAttribute(campo.Attribute("regex").Value);
                    regex.ErrorMessage = campo.Attribute("mensaje").Value;
                    (attributes as List<Attribute>).Add(regex);
                }
            }
        }

        // Se llama al método base de obtención de validadores
        return base.GetValidators(metadata, context, attributes);
    }
}

Global.asax

Configuración del MetadataProvider y el ModelMetadataProvider personalizado en la aplicación.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// Configuración inicial de la aplicación
/// </summary>
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    #region Proveedores de Metadatos Personalizados

    // Proveedor de generación de metadatos personalizado
    ModelMetadataProviders.Current = new CustomModelMetadataProvider();
    // Se elimina el proveedor de validación predeterminado
    ModelValidatorProvider dataAnnotationsModelValidatorProvider =
        ModelValidatorProviders.Providers.
        Single(p => p is DataAnnotationsModelValidatorProvider);
    ModelValidatorProviders.Providers.Remove(dataAnnotationsModelValidatorProvider);
    // Proveedor de validación de metadatos personalizado
    ModelValidatorProviders.Providers.Add(new CustomMetadataValidationProvider());

    #endregion
}

Index.cshtml

Vista de ejemplo en la que se presenta el formulario de captura para visualizar el resultado de todo el proceso anterior.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@using (@Html.BeginForm()) 
{ 
    <div class="form-group">
        @Html.LabelFor(m => m.Nombres)
        @Html.TextBoxMaxLengthFor(m => m.Nombres, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Nombres, "", new { @class = "text-info" })
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ApellidoPaterno)
        @Html.TextBoxMaxLengthFor(m => m.ApellidoPaterno, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.ApellidoPaterno, "", new { @class = "text-info" })
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ApellidoMaterno)
        @Html.TextBoxMaxLengthFor(m => m.ApellidoMaterno, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.ApellidoMaterno, "", new { @class = "text-info" })
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.FechaNacimiento)
        @Html.TextBoxMaxLengthFor(m => m.FechaNacimiento, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.FechaNacimiento, "", new { @class = "text-info" })
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.CorreoElectrónico)
        @Html.TextBoxMaxLengthFor(m => m.CorreoElectrónico, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.CorreoElectrónico, "", new { @class = "text-info" })
    </div>
    <div class="form-group">
        <div>
            <input type="submit" value="Guardar" class="btn btn-default" />
        </div>
    </div>
}
@section Scripts{
    @*Necesario para efectuar las validaciones de metadatos del lado del cliente*@
    @Scripts.Render("~/bundles/jqueryval")
}

De igual forma pueden descargar la solución de ejemplo desde este link: Descargar

Comentarios

Entradas más populares de este blog

Publicación de WCF en Net.TCP

Una de las grandes características de WCF es la forma en como éstos pueden ser expuestos a nuestros clientes, por ejemplo los siguientes protocolos, de manera muy recurrente: Http TCP Net Pipe ¿Sabes en que casos usar cada uno? Http cuando el servicio y el cliente se encuentran en redes diferentes TCP cuando el servicio y el cliente se encuentran en la misma red, pero en equipos diferentes Net Pipe cuando el servicio y el cliente se encuentran alojados en el mismo equipo ¿Por qué razón? Por cuestiones de redimiento y seguridad de la información Si una petición http toma un tiempo de 200 ms, por poner un ejemplo Una petición tcp tomará aproximadamente el 20% menos que la petición http (40 ms aproximadamente) Y una petición Net Pipe , tomará aproximadamente el 80% del tiempo que toma el hacer una petición tcp (32 ms aproximadamente) Para mayor entendimiento, chequen este post que alguna vez consultamos para entender un poco más acerca de éstos conceptos: https://j...

Validación de Fechas con jQuery Validator

Al validar fechas con jQuery Validate nos encontramos con el problema de que dicha validación se hace en formato: MM/DD/YYYY, esto es correcto, pero no tanto para formato de fechas de México al menos que es más común para nosotros hacerlo de manera que dicho formato sea: DD/MM/YYYY. Para poder solucionar este problema haremos algunas modificaciones en las librerías: jquery.validate.js jquery.validate.min.js Veamos el siguiente video para la implementación de dicha solución: jquery.validate.js Se deja comentado el cómo se hace la validación original y se adiciona el código personalizado que permite validar el formato de fecha del tipo: DD/MM/YYYY 1 2 3 4 5 6 7 8 9 10 11 // http://jqueryvalidation.org/date-method/ // Nivisix Soluciones: personalización para formato de fechas DD/MM/YYYY date : function ( value, element ) { //return this.optional( element ) || !/Invalid|NaN/.test( new Date( value ).toString() ); var com...

SQLServer Stored Procedure Para Ordenar Columnas Ascendente / Descendente

Pues este es nuestro primer post referente a SQLServer ... En primera instancia les queremos platicar qué es lo que buscamos:  Mediante un stored procedure obtener un listado ordenado por alguna de las columnas devueltas de manera ascendente o descendente, dependiendo de los parámetros de entrada del mismo. Muchas de las ocasiones cuando nos ha tocado buscar información acerca de cómo hacer este proceso, solemos encontrar recomendaciones de hacerlo usando un query que se contruye en tiempo de ejecución usando una variable de tipo VARCHAR(MAX) y después hacer uso de la función EXEC para ejecutarlo. Al final, se resuelve el problema, pero la intención es mejorar el procedimiento sin afectar el rendimiento y evitando problemas de seguridad como el SQL Injection . Veamos un video con una propuesta diferente de resolución de este requerimiento: Creación de la Base de Datos Usando el siguiente query que pueden ejecutar en el cliente base de datos SQL Server creare...