martes, 11 de mayo de 2010
Una de las mejoras más esperadas de ASP.NET MVC 2 es, sin duda, el sistema integrado de validación del Modelo basado en las anotaciones de datos (Data Annotations). Y aunque la implementación en general es bastante apañada, hay algunos aspectos mejorables, sobre todo cuando intentamos desarrollar aplicaciones en nuestro idioma.
Por ejemplo, existe un curioso comportamiento del juego de herramientas de validación en cliente y servidor en lo relativo a la introducción de decimales en el Modelo. Imaginemos la siguiente entidad de datos, con sus correspondientes anotaciones:
public class Producto
{
[Required(ErrorMessage="*")]
public string Nombre { get; set; }
[DisplayName("Peso (Kg.)")]
[Range(0.1, 10, ErrorMessage="Entre {1} y {2}")]
[Required(ErrorMessage="*")]
public double Peso { get; set; }
}
Centrándonos en la propiedad
Peso
, la intención de sus anotaciones está bastante clara: queremos que sea de introducción obligatoria, y que su valor se encuentre en el rango entre 0,1 y 10 kilogramos.Creamos ahora un formulario de introducción de datos para dicha entidad utilizando como base el generado por defecto por Visual Studio. El código resumido de la vista es el siguiente:
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>
<h2>Create</h2>
<% Html.EnableClientValidation(); %>
<% using (Html.BeginForm()) {%>
... // Omitido
<% } %>
</asp:Content>
Si utilizamos exclusivamente la validación en servidor no habrá problema, pero si, como en el código anterior, activamos en ella la validación en cliente, podemos encontrarnos con un curioso y problemático escenario debido a la falta de sincronización entre las culturas en cliente y en servidor.
Sintomatología
Si en el campo Peso introducimos un entero, por ejemplo un “2”, todo funciona correctamente. El validador en cliente dejará pasar el valor, y en servidor se realizará la conversión de dicho valor adouble
sin problema.Sin embargo, al intentar introducir un valor no entero como “1,23” comienza la fiesta. Si utilizamos como separador de decimales una coma, el validador en cliente no nos permitirá hacer un submit del formulario, quedándonos atrapados en esta capa:
Si en cambio utilizamos como separador un punto, por ejemplo “2.5”, la validación en cliente considerará que el valor decimal es correcto, y permitirá el envío de los datos del formulario. Sin embargo, el servidor intentará obtener el valor
double
utilizando el formato asociado a la cultura actual, en la que el punto no es un carácter válido de separación, por lo que decidirá que el estado del Modelo es inválido, y nos enviará de vuelta al formulario:(Por cierto, hace unas semanas comenté por aquí cómo modificar los mensajes de validación por defecto en ASP.NET MVC 2, como el error mostrado en la captura anterior).
En resumen: no podemos continuar si utilizamos como separador decimal la coma (primer caso), ni tampoco si utilizamos el punto (segundo caso). Literalmente, estamos en un callejón sin salida.
Diagnóstico
El problema se debe básicamente a que los scripts de validación están utilizando únicamente la cultura “en-US”, en la que el carácter de separación decimal es el punto. Es decir, por defecto se utilizan los formatos de fecha y numéricos de la cultura inglesa/americana, y no existe ningún punto en el código de las librerías de scripting MicrosoftAjax o MicrosoftMvcValidation donde se modifiquen estos parámetros.Sin embargo, el tratamiento en servidor se realiza bajo la cultura definida en el hilo de ejecución actual, en este caso la correspondiente a “es-ES”, en la que las comas son los separadores entre la parte entera y la decimal.
Esto es fácil de comprobar introduciendo al final de la vista:
<p>
Cultura en servidor:
<%= System.Threading.Thread.CurrentThread.CurrentCulture.Name %>
</p>
<script type="text/javascript">
alert("Cultura en cliente: " + Sys.CultureInfo.CurrentCulture.name);
</script>
Tratamiento
Es posible que haya formas más sencillas para solucionar esta cuestión, pero de momento la única que he encontrado para sincronizar las culturas es forzar en el lado cliente la utilización de las opciones culturales que estén siendo consideradas en servidor.Para ello, necesitamos asignar durante la inicialización de la página un objeto de tipo CultureInfo a la propiedad Sys.CultureInfo.CurrentCulture. Este objeto contiene los formatos numéricos y de fecha que serán utilizados en las operaciones de conversión llevadas a cabo desde las librerías de scripting Microsoft Ajax, y necesita dicha información durante su instanciación, como en el siguiente ejemplo:
<script type="text/javascript">
var ci = {
"name": "es-ES",
"numberFormat": {información de formato numérico},
"dateTimeFormat": {información de formato de fecha y hora}
};
Sys.CultureInfo.CurrentCulture = Sys.CultureInfo._parse(ci);
</script>
Sin embargo, esto es algo más complejo de lo que podría parecer en un principio. El formato numérico contiene información sobre los separadores de millares, decimales, formas de representar negativos, monedas, dígitos, etc; de la misma forma, respecto a las fechas, es necesario suministrar el formato, nombre de meses, días de la semana, etc. Sin duda, un trabajo demasiado duro para tener que hacerlo a mano.
Por suerte, la representación de los formatos de números y fechas utilizado por las librerías de scripting de Microsoft son idénticas en cliente y servidor, por lo que podemos serializar como JSON los objetos
Thread.CurrentThread.CurrentCulture.NumberFormat
y Thread.CurrentThread.CurrentCulture.DateTimeFormat
y utilizar el resultado para crear el objeto CultureInfo
:<%
JavaScriptSerializer jss = new JavaScriptSerializer();
var cultureInfo = Thread.CurrentThread.CurrentCulture;
string name = cultureInfo.Name;
string numberFormat = jss.Serialize(cultureInfo.NumberFormat);
string dateFormat = jss.Serialize(cultureInfo.DateTimeFormat);
%>
<script type="text/javascript">
var ci = {
"name": "<%= name %>",
"numberFormat": <%= numberFormat %>,
"dateTimeFormat": <%= dateFormat %>
};
Sys.CultureInfo.CurrentCulture = Sys.CultureInfo._parse(ci);
</script>
Pero está claro que no tiene demasiado sentido repetir el código anterior a lo largo y ancho de las vistas de edición de datos donde queramos activar la localización. Seguro que podemos mejorar esto… ;-)
Medicina genérica: El helper Html.EnableLocalizedClientValidation()
Vamos a crear un helper que realice por nosotros estas tareas de la forma más sencilla posible.Html.EnableLocalizedClientValidation()
encapsula el comportamiento del helper estándar EnableClientValidation()
, y añade los scripts de inicialización de las opciones culturales comentados anteriormente. Su uso será como se muestra a continuación:<script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="../../Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>
<%= Html.EnableLocalizedClientValidation() %>
<h2>Create</h2>
<% using (Html.BeginForm()) { %>
// .. El resto del código de vista
La única diferencia respecto al método habitual es que estamos utilizando una expresión de salida
<%= %>
, y que el helper invocado es EnableLocalizedClientValidation()
.El código del helper es:
using System.Text;
using System.Web.Script.Serialization;
using System.Threading;
namespace System.Web.Mvc
{
public static class Extensions
{
public static MvcHtmlString EnableLocalizedClientValidation(this HtmlHelper html)
{
// Habilitamos la validación en cliente
html.EnableClientValidation();
// Obtenemos la información de la cultura actual
JavaScriptSerializer jss = new JavaScriptSerializer();
var cultureInfo = Thread.CurrentThread.CurrentCulture;
string name = cultureInfo.Name;
string numberFormat = jss.Serialize(cultureInfo.NumberFormat);
string dateFormat = jss.Serialize(cultureInfo.DateTimeFormat);
// Generamos el código
StringBuilder sb = new StringBuilder();
sb.Append("<script type=\"text/javascript\">");
sb.AppendLine("var ci = {");
sb.AppendLine(" \"name\": \"" + name + "\",");
sb.AppendLine(" \"numberFormat\": " + numberFormat + ", ");
sb.AppendLine(" \"dateTimeFormat\": " + dateFormat);
sb.AppendLine("};");
sb.AppendLine("Sys.CultureInfo.CurrentCulture = Sys.CultureInfo._parse(ci);");
sb.AppendLine("</script>");
return MvcHtmlString.Create(sb.ToString());
}
}
}
Puedes descargar el código fuente desde mi SkyDrive.
Una última anotación: terminando de escribir este post, observo que el gran Phil Haack (PM de ASP.NET MVC) acaba de publicar una entrada comentando el mismo tema, y enfocando la solución desde otra óptica. Una lástima que no lo haya publicado antes, me habría ahorrado el tiempo que me ha llevado esta investigación, aunque, en cualquier caso, ha valido la pena para entender un poco más los entresijos del framework.
Publicado en: Variable not found
Hey, ¡estoy en twitter!
7 Comentarios:
Excelente post José, la verdad es el tipo de cosas con la que uno se encuentra en el día a día del desarrollo web, particularmente estoy participando actualmente en un proyecto en asp.net mvc y la verdad por lo que se vé, vale la pena contemplar la posibilidad de una migración a asp mvc 2.
Gracias, Frank!
Sin duda, ASP.NET MVC 2 añade características muy interesantes, sobre todo relativas al aumento de la productividad.
Si tenéis oportunidad, no dudéis en dar el salto. :-)
Saludos.
Hola, le veo un problema a tu solución:
El usuario se confundiría con las fechas. Por ejemplo si un usuario español, metiese la fecha de publicación de tu post (12/05/2010) en un servidor estadounidense, el al creer estar trabajando con su cultura habitual, pensaría que la ha metido correctamente. Sin embargo, al haberle cambiado la cultura, lo que realmente quedaría en el servidor sería el 5 de diciembre.
Un saludo
Hola, Lopez, gracias por comentar.
Desde luego, el código utiliza la cultura actual del servidor ("CurrentCulture"), por lo que se aplicarían las opciones de ésta. Por tanto, si el servidor es americano no habría problemas con las fechas, como indicas en tu comentario, sino también con los valores decimales.
Para que esto fuera sensible al idioma del usuario e independiente del servidor, haría falta establecer la cultura del Thread que procesa la petición, o utilizar las opciones de globalización del web.config.
Saludos!
Buenas José, Sabes si para MVC 3 es igual? es que lo he realizado igual que en tu post... y no me ha salido!!
Saludos y Gracias, una vez mas, por tus aportaciones y ayuda.
Salvador Anaya
Hola, Salvador.
El sistema de validación ha cambiado bastante con MVC 3.
Puedes leer algo sobre ello en este artículo.
Espero que te valga.
Saludos.
Cómo se tendría que cambiar para que funcione en Razor?
Enviar un nuevo comentario