ASP.NET MVC: Create tus propias validaciones

Una de las noverdades de ASP.NET MVC 2 es que lleva integrado el uso de Data Annotations para permitirnos validar los modelos. En ASP.NET MVC 1 también era posible pero no era un proceso tan integrado como con la nueva versión.

Mediante Data Annotations podemos indicar un conjunto de reglas que deben cumplir las propiedades de nuestros modelos. Así para indicar que el campo Login es obligatorio basta con decorar la propiedad correspondiente:

public class UserData
{
[Required(ErrorMessage="El nombre de usuario NO puede estar vacío")]
public string Login { get; set; }
}

Y luego dejar que el sistema de ASP.NET MVC 2 haga “la magia”: cuando el model binder deba reconstruir el objeto UserData a partir de los datos de la request, si no existe el dato para la propiedad Login, automáticamente nos pondrá el ModelState a inválido:

[HttpPost()]
public ActionResult Index(UserData data)
{
if (!ModelState.IsValid) {
// Hay errores en el modelo (data.Login está vacío)
return View(data);
}

// El modelo es correcto... realizar tareas
}

Pero bueno… lo normal es que los atributos que vienen de serie se te queden “cortos” y que tarde o temprano necesites crear tus propias validaciones… y este es el motivo de este post.

Vamos a realizar un ejemplo sencillo: crearemos una validación que indique si una propiedad string contiene una representación válida de un número entero. Vamos a soportar notación decimal, hexadecimal (prefijada por 0x), octal (prefijada por 0) y binaria (prefijada por 0b). Así:

  • 1234567890 es una cadena válida (decimal)
  • 0xaa112d es una cadena válida (hexadecimal prefijada por 0x)
  • 01239 es una cadena inválida (prefijo 0 indica octal y 9 no es carácter octal).
  • 0b00112101 es una cadena inválida (prefijo 0b indica binario y el carácter 2 no es binario).

Vamos a ello?

1. Creación de la validación en el servidor

Para crear una validación en el servidor debemos crear un nuevo atributo que herede de ValidationAttribute y que contenga él código de la validación sobrecargando el método IsValid:

public class DeveloperIntegerAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
bool valid = false;
if (value is string && value != null)
{
string sval = value as string;
if (sval.StartsWith("0x") || sval.StartsWith("0X"))
{
valid = CheckChars(sval.Skip(2), "0123456789abcdefABCDEF");
}
else if (sval.StartsWith("0b") || sval.StartsWith("0B"))
{
valid = CheckChars(sval.Skip(2), "01");
}
else if (sval.StartsWith("0"))
{
valid = CheckChars(sval.Skip(1), "01234567");
}
else
{
valid = CheckChars(sval, "0123456789");
}
}
return valid ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}

private bool CheckChars(IEnumerable<char> str, string validchars)
{
return str.All(x => validchars.Contains(x));
}
}

Como podéis ver el código es realmente sencillo: debemos comprobar que el parámetro “value” satisface nuestras condiciones y en este caso devolver ValidationResult.Success y en caso contrario devolver un ValidationResult con el mensaje de error asociado.

Ahora ya podemos aplicar nuestro nuevo flamante atributo [DeveloperInteger] a nuestras clases de modelo:

public class FooModel
{
[Required]
[DeveloperInteger(ErrorMessage="Numero no correcto (se aceptan prefijos 0 - octal, 0x - hexa, 0b - binario y ninguno para decimal")]
public string DeveloperNumber { get; set; }

[Required]
public string OtraCadena {get; set;}
}

Y listos! Con esto la validación ya está integrada en el sistema de ASP.NET MVC. Podemos comprobarlo si creamos una vista para crear datos de tipo FooModel:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<CustomValidations.Models.FooModel>" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Index</title>
</head>
<body>
<% using (Html.BeginForm()) {%>
<%: Html.ValidationSummary(true) %>
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
<%: Html.LabelFor(model => model.DeveloperNumber) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.DeveloperNumber) %>
<%: Html.ValidationMessageFor(model => model.DeveloperNumber) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.OtraCadena) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.OtraCadena) %>
<%: Html.ValidationMessageFor(model => model.OtraCadena) %>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
<% } %>
<div>
<%: Html.ActionLink("Back to List", "Index") %>
</div>
</body>
</html>

Este código es el código estándard que genera Visual Studio si añadimos una vista tipada para Crear objetos FooModel. Fijaos en el uso de ValidationMessageFor para mostrar (en caso de que el modelo no esté correcto) los mensajes de error correspondientes.

Y finalmente como siempre en el controlador, el par de métodos para mostrar la vista para entrar datos y para recoger los datos y mirar si el modelo es válido:

public ActionResult Index()
{
return View();
}

[HttpPost()]
public ActionResult Index(FooModel data)
{
if (!ModelState.IsValid)
{
return View(data);
}
else
{
// modelo es correcto
return RedirectToAction("Hola");
}
}

Más fácil imposible, no??? 🙂

2. Validación en Javascript

Crear validaciones en servidor es muy fácil, pero ahora lo que se lleva es validar los datos en el cliente. Esto hace nuestra aplicación más amigable ya que le evitamos esperas al usuario (no enviamos datos incorrectos al servidor).

Nota: Se ha repetido innumerables veces pero no está de más decirlo de nuevo: Las validaciones en cliente no pueden sustituir nunca a las validaciones en servidor. El objetivo de validar en cliente no es garantizar la seguridad ni la consistencia de los datos, és únicamente proporcionar mejor experiencia de usuario. Siempre debe validarse en servidor, siempre!

Para validar en cliente debemos realizar tres pasos: Habilitar la validación de cliente en la vista, activarla en servidor y crear el código javascript. Veamos cada uno de esos pasos.

El primero es el más fácil, basta con añadir la llamada al método EnableClientValidation() que está en el HtmlHelper. Este método es de la Microsoft Ajax Library así que debéis referenciarla con tags <script>:

<head runat="server">
<script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script type="text/javascript" src="../../Scripts/MicrosoftMvcValidation.js"></script>
<title>Index</title>
</head>
<body>
<% Html.EnableClientValidation(); %>

De esta manera veréis p.ej. que automáticamente ya nos valida si los campos de texto están vacíos (porque usábamos [Required]). Obviamente nuestra validación propia, no la realiza en javascript… veamos como podemos hacerlo.

Primero debemos activar la validación en servidor, y eso se consigue creando una clase derivada de DataAnnotationsModelValidator<TAttr> donde TAttr es el tipo del atributo que tiene la validación, en nuestro caso DeveloperIntegerAttribute. En esta clase debemos redefinir el método GetClientValidationRules para devolver la lista de validaciones en cliente a ejecutar (métodos javascript a llamar).

public class DeveloperIntegerValidator : DataAnnotationsModelValidator<DeveloperIntegerAttribute>
{
private string message;
public DeveloperIntegerValidator(ModelMetadata metadata, ControllerContext cc, DeveloperIntegerAttribute attr)
: base(metadata, cc, attr)
{
message = attr.ErrorMessage;
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule()
{
ValidationType = "devinteger",
ErrorMessage = message
};
// Aquí podríamos añadir parámetros a la función javascript:
// rule.ValidationParameters.Add("parametro", valor);
return new[] { rule };
}
}

Fijaos que devolvemos una colección de objetos ModelClientValidationRule que representan los métodos javascript a llamar para realizar nuestra validación (podemos asociar más de uno).

Un paso adicional que debemos realizar es indicar que la clase DevelolperIntegerValidator gestiona las validaciones en cliente para DeveloperIntegerAttribute y para ello añadimos la siguiente línea en global.asax (p.ej. en Application_Start):

DataAnnotationsModelValidatorProvider.RegisterAdapter(
typeof(DeveloperIntegerAttribute), typeof(DeveloperIntegerValidator));

Ahora ya sólo nos queda crear nuestro método javascript. Realmente no tenemos una función llamada devinteger sino que accedemos a Sys.Mvc.ValidatorRegistry.validators y establecemos una entrada llamada devinteger cuyo valor es un método que recoge los parámetros de validación (si hubiese, en nuestro caso no hay) y devuelve otra función que es la que realiza la validación…

Sí, parece complicado pero tampoco lo es tanto:

Sys.Mvc.ValidatorRegistry.validators["devinteger"] = function (rule) {
// Si tuvieramos un parametro llamado parametro lo recogeríamos aqui:
// var parameter = rule.ValidationParameters["parametro"];
// Debemos devolver la función que realiza la validación
return function (value, context) {
var svalue = "" + value;
var chars = "0123456789";
var radix = 10;
var start = 0;
if (svalue.substr(0, 2) == "0x" || svalue.substr(0, 2) == "0X") { chars = "01234567890abcdefABCDEF", start = 2; }
else if (svalue.substr(0, 2) == "0b" || svalue.substr(0, 2) == "0B") { chars = "01"; start = 2; }
else if (svalue.search(0, 1) == "0") { chars = "01234567"; start = 1; }

return (function (str, chars) {
var ok = true;
for (var i = 0; i < str.length; i++) {
var char = str.charAt(i);
ok = chars.indexOf(char) != -1;
if (!ok) break;
}
return ok;
})(svalue.substring(start), chars) ? true : rule.ErrorMessage;
};
};

Fijaos en el return function (value, context). Aquí devolvemos la función anónima que realmente realiza la validación. Dicha función recibe dos parámetros: value, que es el valor a evaluar y context que es un contexto de validación. El código dentro de esta función anónima debe devolver true si el value valida satisfactoriamente y la cadena de error en caso contrario.

Y listos! Ya tenemos la validación por javascript en cliente! Si alguien sabe alguna manera mejor de realizar dicha validación en javascript que me lo diga (sí, javascript no es mi fuerte). Yo intenté usar parseInt, pero dado que parseInt valida los carácteres válidos hasta que encuentra el primer inválido no me servia (para mi 0x1j es inválido y no el número 1).

Y listos! Ya tenemos la validación en cliente para nuestro validador propio… Que os parece? Fácil, no???? 🙂

Un saludo!

7 comentarios sobre “ASP.NET MVC: Create tus propias validaciones”

  1. Como de costumbre, un magnífico post, Eduard.

    El sistema de validaciones de MVC es una pasada en cuanto a su flexibilidad. Imho, lo único que queda un poco «sucio» es la implementación del javascript de forma independiente, en el que muchas veces hay que codificar nuevamente el procedimiento de validación que ya hemos hecho en C# con anterioridad.

    Saludos & enhorabuena por el post!

  2. ¡Buen post Eduard!

    Como dice José M., el tema del javascript es el que queda más colgandero. Realmente la mágia de DataAnnotations y MVC estaba en que la misma validación que usábamos en el servidor era válida para el cliente… Si realizamos validaciones personalizadas deberíamos mantener dos validaciones distintas para un mismo caso 🙁
    Aún así me parece útil para seguir la misma dinámica en cuanto a validaciones…
    De todos modos, es posible que algunas de las validaciones que no podemos concretar con StringLength, Required, etcétera las podamos cubrir con expresiones regulares.

    ¡Saludos!

  3. Hola a los dos!
    Gracias por vuestros comentarios!! 😉

    Ambos tenéis razón en que el tema del javascript és el que está peor tratado… Supongo que es el handicap por tener dos lenguajes distintos (el de servidor y el de cliente).
    Cierto que con las validaciones por defecto no tenemos que hacer nada, pero claro eso es porque alguien «ha hecho el javascript por nosotros», no porque se reaproveche el código C# que sería lo realmente interesante. 😛

    Por otro lado el sistema es «extensible» (en el sentido de que un proveedor puede proporcionarnos el assembly con las validaciones de servidor + el js con las validaciones en cliente)… así que supongo que en un futuro veremos «librerías» de validaciones complejas (o no, quien sabe, porque yo como pitoniso… :p).

    Y ya, pensando en voz alta, reaprovechar el código C# para validar sería factible si realizáramos una petición ajax a una acción de un controlador que ejecutase la validación y enviase el resultado. Eso tendría una ventaja y es que nos permite entrar en el campo de validaciones «de dominio»… p.ej. si el login de usuario ya existe, etc… A ver si saco un poco de tiempo y pruebo de montar algo así integrado con DataAnnotations para ver si funciona y es factible.

    En fin, todo un mundo eso de las validaciones!!! 🙂

    Gracias de nuevo por comentar!!!

  4. Hola Eduard :),

    Obvio que las validaciones incluidas por defecto van acompañadas de su javascript, ¡Si no sí que sería magia! 😀

    El caso es que, parece que todo lo que se escape de cosas básicas o bien haces expresiones regulares o bien combinas las existentes.

    El tema del Ajax… hombre es otra de las opciones para reutilizar la lógica aunque no deja de ser una llamada a servidor y se escapa de nuevo del cliente. Mejora parcialmente la experiencia de usuario pero … ¿Ir a validar para luego volver de nuevo si está todo ok? … Puede tener sentido para un formulario donde compruebes la disponibilidad de un nombre de usuario, dominio, etcétera… A parte de eso…

    Quizás para opciones algo más personalizadas la opción que propones es una opción acertada aunque con el peligro que ello trae… Me explico: Si las validaciones son algo más complejas de lo que nos están ofreciendo, es posible que hacer dichas validaciones en el cliente no sean buena idea. Recuerdo un proyecto al respecto y también las métricas de rendimiento que le acompañaban de la cantidad de tiempo que se empleaba en las validaciones en cliente.

    ¡Saludos!

  5. @Gisela
    Jajajajaaa… pero molaría eh, que fuese magia??? Si es que esos de microsoft no se esfuerzan… 😛 😛 😛

    El tema de Ajax, obviamente, no tiene sentido para validaciones «sintácticas», es evidente que realizar una llamada Ajax para comprobar si un número es par sólo para reaprovechar el código en C# es… bueno… inexplicable (aunque cosas más raras termina uno viendo por esos mundos de dios) 😛
    Para validaciones del dominio donde se requieran datos de servidor tiene su sentido, y estuve dando algunas vueltas sobre como se podría integrar esas validaciones con DataAnnotations y el sistema de validaciones de MVC y no llegué a ninguna conclusión satisfactoria (tampoco pensé mucho, la verdad)…

    Gracias por comentar de nuevo!!!

    Saludos!!!! 😉

  6. Holas,

    este tema de la magia es interesante… realmente hay veces que es más por afición que por necesidad real, pero mola. 🙂

    Precisamente hace algún tiempo estuve mirando Script#, por si podría aportar alguna solución al respecto. Esta herramienta se utiliza mucho para desarrollar scripts, pues es capaz de generar código js a partir de C#, y pensé que quizás por ahí podría ir la solución a este tema; si podía generar automáticamente el script desde el código de validación en servidor, evitaría el doble mantenimiento. La conclusión fue que no… 🙁

    También dediqué un ratico a ver si existían soluciones capaces de generar una estructura CodeDOM de un ensamblado, pero no encontré nada utilizable…

    Al final la única conclusión a la que llegué es que se podía conseguir algo en ocasiones en que la validación podía llevarse a un árbol de expresión; así, en servidor podría utilizarse su «versión compilada», y a su vez utilizarlo para generar automáticamente el código cliente (como hace, por ejemplo, el proveedor de EF).

    En fin, pajas mentales de un rato de ocio… ;-D

    Saludos.

  7. Jejejeee…
    A mi también me encanta realizar pruebas de esas cuando tengo un poco de tiempo libre… efectivamente la mayoría de veces es más por afición que por necesidad 🙂

    En fin… que haríamos sin todas esas pajas mentales??? 🙂

    Un saludo!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *