martes, 2 de julio de 2013

Validación en cliente en ASP.NET MVC

En este post quiero explicar cómo llevar a cabo validaciones personalizadas del modelo en ASP.NET MVC. Además, también veremos cómo implementar validación de cliente para nuestros atributos personalizados.

Para el resto del post nuestro único propósito será validar que la dirección de correo electrónica pedida al usuario pertenece a un dominio concreto. Por ejemplo, sólo aceptaremos correos con el dominio tabconsultores.com o tabconsultores.net.

Inicialmente, el siguiente código servirá a nuestro propósito tanto en servidor como en cliente:

public class Usuario

{

    [Required]

    [RegularExpression("@tabconsultores.com$",

        ErrorMessage = "El dominio no es válido.")]

    public string Email { get; set; }

}

En cliente funciona porque el DataAnnotation RegularExpression sabe implementarse en cliente a través de los atributos data-val-regex-pattern y data-val-regex.

clip_image002[4]

En realidad, estos atributos data más el plugin jquery.validate.unobtrusive.js son los que hacen la magia de que jquery.validate valide correctamente en cliente nuestra expresión regular. Si te fijas bien, por cada regla de validación de cliente, se añade un atributo con la forma data-val-regla=”mensaje de error” (p. ej. data-val-regex=”...”) y además por cada parámetro de cada regla de validación se añade también un atributo con la forma data-val-regla-parámetro=”valor” (p. ej. data-val-regex-pattern=”…”).

Como queremos ser un poco más expresivos a la hora de decorar nuestras entidades, probaremos ahora un nuevo acercamiento que consiste en crear un nuevo atributo personalizado que herede de RegularExpressionAttribute:

public class Usuario

{

    [Required]

    [EsEmailInterno(ErrorMessage = "El dominio no es válido.")]

    public string Email { get; set; }

}

 

public class EsEmailInternoAttribute : RegularExpressionAttribute

{

    public EsEmailInternoAttribute()

        : base(".+@tabconsultores\\.(com|net)$")

    {

    }

}

Aunque hemos ganado en claridad, ahora la validación de cliente ya no sabe cómo llevar a cabo la validación. El código HTML generado es distinto al anterior, ya no incluye ningún atributo data relacionado con nuestro atributo personalizado EsEmailInterno:

clip_image004[4]

Para solucionar esto y ayudándonos de la circunstancia de que nuestro atributo hereda de RegularExpressionAtribute, podemos incluir el siguiente código en el global.asax para registrar durante la inicialización de la aplicación, que nuestro nuevo atributo utilizará la implementación predeterminada que viene de serie con RegularExpressionAttribute.

DataAnnotationsModelValidatorProvider.RegisterAdapter(

        typeof(EsEmailInternoAttribute),

        typeof(RegularExpressionAttributeAdapter));

Ahora nuestro atributo ya valide correctamente en cliente.

Aunque esta vez hemos logrado salvar la validación en cliente a través de la implementación predeterminada de RegularExpressionAttribute, lo más normal es que nuestros atributos de validación personalizada no siempre puedan aprovecharse de esta circunstancia. Por ejemplo, si complicamos un poco más el ejemplo inicial, ahora lo que haremos será pedir a nuestro atributo que el correo especificado este en una lista concreto de correos (cosa rara, lo sé, pero para los ejemplos soy muy malo).

Siendo así, nuestro nuevo, mejorado e inservible atributo queda de la siguiente manera en el servidor:

public class Usuario

{

    [Required]

    [EsEmailInternoAttribute("sergio@sergio.com", "antonio@antonio.com", ErrorMessage = "El dominio no es válido.")]

    public string Email { get; set; }

}

 

public class EsEmailInternoAttribute : ValidationAttribute

{

    private readonly string[] _emails;

 

    public EsEmailInternoAttribute(params string[] emails)

    {

        _emails = emails;

    }

 

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)

    {

        if (value != null && _emails.Contains(value.ToString()))

        {

            return ValidationResult.Success;

        }

        return null;

    }

}

Ahora hemos heredado de ValidationAttribute y hemos sobrescrito el método IsValid. Lógicamente la validación sólo ocurre en el servidor, así que lo que tenemos que hacer ahora es suministrar nosotros mismos la validación de cliente. Para ello tendremos que hacer dos cosas:

·         Implementar la interfaz IClientValidatable y el método GetClientValidationRules.

·         Suministrar código javascript que realice la validación en cliente.

Primero la interfaz IClientValidatable

public class EsEmailInternoAttribute : ValidationAttribute, IClientValidatable

{

    private readonly string[] _emails;

 

    public EsEmailInternoAttribute(params string[] emails)

    {

        _emails = emails;

    }

 

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)

    {

        if (value != null && _emails.Contains(value.ToString()))

        {

            return ValidationResult.Success;

        }

        return null;

    }

 

    public IEnumerable<ModelClientValidationRule>

        GetClientValidationRules(

            ModelMetadata metadata,

            ControllerContext context)

    {

        var modelClientValidationRule = new ModelClientValidationRule

        {

            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())

        };

        modelClientValidationRule.ValidationParameters.Add("emails",

            string.Join(",", _emails));

        modelClientValidationRule.ValidationType = "esemailinterno";

        yield return modelClientValidationRule;

    }

}

Al implementar esta interfaz lo que estamos consiguiendo es que se incluyan los atributos data que nosotros queramos en el marcado de la vista. Por ejemplo, además del mensaje de error también hemos decidido volcar el valor de la propiedad _emails porque la necesitamos para la validación de cliente:

clip_image006[4] 

Después de tener ya preparados todos nuestros atributos data, llega el momento de escribir el código javascript necesario para validar realmente nuestro atributo en cliente. Para ello crearemos un fichero llamada esemailinterno.js (el nombre aquí no es relevante) que tendremos que incluir en la vista después de la posición donde hayamos incluido el bundle de jqueryval:

(function ($) {

    $.validator.unobtrusive.adapters.addSingleVal("esemailinterno", "emails");

    $.validator.addMethod('esemailinterno', function (value, element, emails) {

        if (emails.split(",").indexOf(value) != -1) {

            return true;

        }

        return false;

    });

})(jQuery);

¡Y con esto ya tenemos un atributo personalizado de validación que, felizmente, también realiza la validación en cliente!

Realmente, lo interesante del código javascript anterior es entender que es una secuencia de 2 pasos claramente diferenciados. Por un lado, utilizamos el plugin unobtrusive para registrar un nuevo adaptador que leerá los atributos data que correspondan con “esemailinterno” (parámetro adapterName del método addSingleVal) y los registrará como datos a validar en el plugin jquery.validate. Por el otro lado, después registramos un nuevo método de validación en jquery.validate que será quien tenga que llevar finalmente a cabo la validación. Puedes ver más sobre cómo trabaja jquery.validate en este otro post.

En nuestro ejemplo hemos utilizado el método addSingleVal (porque requeríamos pasar el parámetro emails al método de validación), pero hay otros métodos disponibles en unobtrusive. Por ejemplo, con addBool se asume que el método de validación no necesita ningún dato extra para funcionar. Si quisiéramos implementar nuestro ejemplo con addBool podríamos hacerlo de la siguiente forma:

(function ($) {

    $.validator.unobtrusive.adapters.addBool("esemailinterno");

    $.validator.addMethod('esemailinterno', function (value, element) {

        var emails = $(element).data("valEsemailinternoEmails");

        if (emails.split(",").indexOf(value) != -1) {

            return true;

        }

        return false;

    });

})(jQuery);

La diferencia está en cómo recogemos ahora el parámetro emails. Con addSingleVal lo recibíamos como parámetro (claramente, parece mejor opción), pero con addBool tenemos que extraerlo del elemento que estamos validando. Suerte que jQuery hace un tratamiento especial de los atributos data y nos permite que leerlos sea muy sencillo.

Si quieres ver todos los adaptadores disponibles y su referencia, puedes consultarla en Unobtrusive Client Validation in ASP.NET MVC 3.

Otros tips que me han resultado útiles a la hora de implementar la validación en cliente son los siguientes:

Si cargas mediante un Ajax una vista parcial que incluye atributos de validación, tendrás después que llamar manualmente a unobtrusive para parsear el contenido recién insertado el DOM y que sea efectiva la validación antes de enviar el formulario.

$.validator.unobtrusive.parse(formulario);

Si además quieres validar algo especial antes del envío del formulario, y una vez todos los atributos hayan pasado la validación, puedes utilizar la función submitHandler. Esta función, si está registrada, además nos obliga a que seamos nosotros mismos quienes enviemos el formulario:

$(function () {           

    $("form").validate().settings.submitHandler = function (form) {

        // hacer algo...

        form.submit();

    };

});

Es importante ver como el envío del formulario se realiza a través del método nativo del elemento form y no a través del método de jQuery $(form).submit(). De este modo, la validación no se tendrá en cuenta y no volverá a saltar metiéndonos en un bucle infinito. Por otro lado, si el formulario no tiene ninguna validación, submitHandler no saltará cuando se envíe el formulario (es decir, sin validación no te vale este evento para hacer algo antes de enviar).

También es importante el momento en el que establecemos la función asociada a submitHandler. Tiene que ser después de que se haya ejecutado unobtrusive, y como éste se ejecuta en el evento ready pues nosotros también tenemos que hacerlo en un evento ready después de que se haya ejecutado el primero.

Otra cosa a tener en cuenta es que, por defecto, sólo valida campos visibles (aunque siempre podemos hacer que los valide limpiando la propiedad ignore – que por defecto viene con el selector :hidden - con una instrucción como la siguiente $("tuForm").validate().settings.ignore = "";). Además, en en cualquier momento se puede validar el formulario o un elemento en concreto llamando al método valid. Por último, si te da por querer validar campos readonly (a mí me ha pasado), ten en cuenta que versiones recientes de jQuery.Validation no los tienen en cuenta, así que tocará hacer algún hack.

Bueno, espero que te haya servido este post y ahora sepas un poco mejor como funciona y cómo implementar la validación en cliente con MVC.

Un saludo!