miércoles, 16 de junio de 2010

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!



PD: Como siempre… esto es un crosspost desde mi blog en geeks.ms. Pásate mejor por allí!!! :)

No hay comentarios: