live()
, $.browser()
y otros cambios de envergadura.Nuestro código podemos modificarlo teniendo en cuenta los cambios de esta versión, hasta ahí sin problema. El dolor viene con los componentes de terceros sobre los que no tenemos control.
No hace demasiado, Eduard Tomás hablaba de los problemas con unobstrusive ajax de MVC, pero hay muchos más reportados, como jqGrid, el célebre plugin para la creación de rejillas de datos.
Publicado por José M. Aguilar a las 9:15 a. m.
Etiquetas: actualidad, jquery, trucos
Los mensajes de error asociados a cada validador son almacenados inicialmente en atributos
data-val-*
sobre el control a comprobar, y cuando se detecta un problema de validación, son mostrados copiando su contenido al interior de la etiqueta <span>
que el helper Html.ValidationMessage()
habrá generado sobre la página.Sin embargo, al hilo de una consulta reciente en los foros de ASP.NET MVC en MSDN, perfectamente contestada por el amigo Eduard Tomás, pensé que realmente tenemos poco control sobre cómo se muestran estos errores, así que me he puesto un rato a ver cómo podíamos conseguir introducir lógica personalizada en este punto aprovechando la flexibilidad que ofrece jQuery validate 1.9.
Salvo por la escasez de documentación de este componente, tomar el control en el momento de mostrar los mensajes de error es bastante sencillo. Basta con establecer una función en la propiedad
showErrors
de los settings
del plugin, cosa que podemos hacer con el siguiente script de inicialización:<script type="text/javascript">
$(function () {
var settings = $.data($('form')[0], 'validator').settings;
settings.showErrors = function (errorMap, errorList) {
// Aquí el código personalizado:
[...]
// Y si nos interesa, finalmente podemos
// ejecutar el comportamiento por defecto
this.defaultShowErrors();
};
});
</script>
(Por simplificar, estamos asumiendo que en el formulario hay un único tag <form>
, que es el que capturamos con el selector).La función
showErrors()
recibe dos parámetros. El primero es un “mapa” donde asociamos a cada clave (nombre del campo) el mensaje de error que tenga asociado. Así, por ejemplo, el valor de errorMap.Nombre
será nulo si el campo “Nombre
” del formulario no tiene ningún error (ha validado correctamente), o el texto del error en caso contrario.En el segundo parámetro de la función encontraremos un array con los errores a mostrar. En cada elemento tendremos disponible la propiedad
element
, desde la que podemos acceder al control que ha generado el error, y message
, donde podemos consultar o establecer la descripción del mismo.Es importante tener en cuenta que la función
showErrors()
es invocada con mucha frecuencia durante el proceso de edición (pérdida de foco, obtención de foco, pulsación de teclas…), por lo que desde el punto de vista de la usabilidad no tiene demasiado sentido introducir en ella procesos bloqueantes (como puede ser un alert()
) o demasiado largos en el tiempo, para evitar que se solapen.Por ejemplo, en el siguiente código utilizamos el efecto “highlight” de jQuery UI para resaltar con un rápido destello el elemento en el que se ha detectado un error:
settings.showErrors = function (errorMap, errorList) {
for (var i = 0; i < errorList.length; i++) {
var error = errorList[i];
// error.element es el elemento que ha provocado el error
$(error.element).effect("highlight", { times: 1 }, 100);
}
this.defaultShowErrors();
};
En fin, algo no demasiado útil ;-P, pero interesante en cualquier caso para profundizar un poco en los misterios e interioridades de jQuery validate.Publicado en Variable not found.
Publicado por José M. Aguilar a las 10:33 a. m.
Etiquetas: aspnetmvc, jquery, scripting, trucos, validadores
file
(el que usamos para hacer los uploads) de un formulario, por ejemplo, para evitar que el usuario envíe un archivo que por cualquier motivo no deba ser subido al servidor.O dicho de otra forma, imaginemos la siguiente porción de un formulario en pantalla, que podría ser generada con el código que podéis ver justo a continuación:
<label for="archivo">Archivo a enviar:</label> <input type="file" id="archivo" name="archivo" /> <input type="button" onclick="limpiarInputFile('archivo');" value="Limpiar" />
Y la pregunta en este momento sería, ¿qué código deberíamos implementar en la función
limpiarInputFile()
que estamos utilizando en el evento onclick
si quisiéramos limpiar o inicializar el contenido del campo archivo
?Aunque de forma intuitiva podría parecer que basta con establecer el valor del campo a una cadena vacía, una prueba rápida nos demostrará que esto no es posible. Desde hace ya tiempo, por motivos de seguridad, los navegadores no permiten el acceso de escritura a la propiedad value en los campos de envío de archivos, por lo que nos encontramos una vía sin salida. Esto lo vemos con el siguiente código, utilizando jQuery:
function limpiarInputfile(id) {
var input = $('#' + id);
var nuevoValor = "c:\\windows\\system32\\mspaint.exe";
alert(input.val()); // Muestra "C:\Datos.dat"
input.val(nuevoValor); // Establecemos un nuevo valor
alert(input.val()); // ¡¡Muestra "C:\Datos.dat"!!
}
Como vemos, somos vilmente ignorados cuando intentamos establecerle un valor.
Pues bien, una posible solución consiste en eliminar del DOM el elemento
<input type="file">
y volver a crearlo justo después en el mismo lugar. He visto por ahí varias implementaciones que obligaban a introducir este elemento dentro de un contenedor, pero he creado otra que creo que es más sencilla e igual de efectiva: function limpiarInputfile(id) {
var input = $('#' + id);
var clon = input.clone(); // Creamos un clon del elemento original
input.replaceWith(clon); // Y sustituimos el original por el clon
}
Y eso es todo :-). Observad que lo único que hacemos es crean un clon del elemento original cuyo
value
por supuesto estará en blanco (recordad que esta propiedad no se puede establecer), y justo a continuación eliminamos el elemento original sustituyéndolo por este clon.Si queremos generalizar este código e implementar esta funcionalidad de forma no intrusiva podríamos hacer lo siguiente:
$(function () {
$("input[type=file]").after(
"<input type='button' class='limpiar-inputfile' value='Limpiar'>"
);
$(".limpiar-inputfile").click(function () {
var input = $(this).prev("input[type=file]");
var clon = input.clone();
input.replaceWith(clon);
return false;
});
});
Este código añade automáticamente un botón “Limpiar” a continuación de todos los
<input type=file>
de la página, implementando en el manejador del evento click
la lógica de inicialización del componente que hemos visto antes. De esta forma, sólo se introducirá en la página el botón de limpiado cuando estén activados los scripts, que es en el único momento en que su ejecución tendrá sentido con la solución propuesta.Espero que os sea de utilidad.
Publicado en: Variable not found.
Hace tiempo traté el tema por aquí, y aporté una solución para la versión 2 de ASP.NET MVC, que aún utilizaba las bibliotecas de scripting de Microsoft Ajax. Sin embargo, la versión 3 ha sustituido “de serie” esos componentes por jQuery Validate y el magnífico sistema de validaciones no intrusivas, por lo que todo lo dicho en aquella ocasión no vale ya para nada :-(
El problema radica en que el plugin jQuery Validate utiliza únicamente el punto como separador de decimales, por lo que la validación en cliente de valores de tipo decimal, float o double que utilicen la coma finalizará siempre en un error e impedirá el envío del formulario, como puede observarse en la captura de pantalla de la derecha.
Por cierto, antes de que se me olvide, hace unos meses reportaron este asunto como bug en Microsoft Connect. Si el tema os preocupa, podéis ir y votarlo a ver si conseguimos que este asunto se tenga en cuenta en próximas revisiones.
Sin embargo, estrictamente hablando, no se trata de un bug de ASP.NET MVC, puesto que la validación en cliente ha sido delegada por completo al plugin de jQuery, y éste es el que no tiene en cuenta los aspectos de internacionalización. Desde este punto de vista, quizás tendría más sentido, por tanto, esta issue reportada en Github sobre jQuery Validate, que propone su integración de forma nativa con jQuery Global.
Por tanto, me temo que se trata de un asunto de responsabilidad compartida (y dispersa, por tanto) entre los equipos de MVC, de jQuery Validate, y no sé si de alguno más. Esperemos que entre todos puedan solucionar de forma razonable el problema.
En cualquier caso, los que ya estamos creando aplicaciones con MVC 3 no podemos esperar las soluciones oficiales, que seguro llegarán más tarde o más temprano, y nos vemos obligados a buscar alternativas que nos permitan convivir con este problema de la forma más cómoda posible.
Y esto es lo que veremos en este post: varias posibilidades que tenemos para que la validación en cliente de valores decimales no nos compliquen demasiado la vida. Seguro que hay más, seguro que las hay mejores, pero ahí van unas cuantas opciones que nos pueden ayudar en escenarios como el descrito anteriormente.
1. Desactivar la validación en cliente
Está claro que el problema es en cliente, por lo que si desactivamos estas validaciones y dejamos que sea el servidor el que se encargue de comprobar que los valores de los distintos campos cumplen las restricciones impuestas por su tipo y las anotaciones de datos, ya no nos afectará más la absoluta indiferencia de jQuery Validate hacia las particularidades culturales.Esto podemos conseguirlo de varias formas:
- desactivar la validación en cliente de forma global, estableciendo a
false
la propiedadclientValidationEnabled
en el web.config, lo cual dejará a toda la aplicación sin validaciones en cliente. Como solución es algo drástica, pero poderse se puede. - desactivar la validación en cliente de forma local, sólo en aquellos formularios en los que existan propiedades de tipo decimal, introduciendo el siguiente código Razor (o su correspondiente en ASPX) antes de la llamada a
BeginForm()
:@{ Html.EnableClientValidation(false); }
- desactivar la validación en cliente sólo en el campo que nos interese, que podemos conseguir introduciendo el siguiente script, imaginando que el campo decimal en el que queremos anular la validación en cliente tiene como identificador “Altura”:
<script type="text/javascript"> $("#Altura").removeAttr("data-val"); </script>
2. Modificar jQuery Validate
Esta es una solución algo bestia que he encontrado por ahí, pero soluciona el problema de un plumazo: modificar el código de jQuery Validate para que acepte comas en lugar de puntos para separar los dígitos decimales de los enteros tanto en la validación numérica como en los rangos.En el blog de Lenard Gunda podéis encontrar de forma muy detallada los cambios a realizar al archivo jquery.validate.js (o a su versión minimizada). Hay, sin embargo, un par de detalles que debemos tener en cuenta si optamos por esta solución:
- primero, que nos estamos separando de la distribución oficial del plugin. Si actualizamos la biblioteca jquery.validate, por ejemplo utilizando Nuget, volveremos a tenerlo todo como al principio, y tendremos que volver a introducir los cambios oportunos.
- segundo, que esto no nos ayudará en aplicaciones adaptadas a varios idiomas; si modificamos el plugin para que acepte comas como separador, ya no volverá a aceptar el punto. Una solución rápida que se me ocurre para esto es tener dos versiones de la biblioteca (la original y la modificada), y referenciar desde la página la apropiada para la cultura actual.
3. Modificar la forma en que jQuery Validate parsea los decimales
Afortunadamente, el plugin de validación para jQuery es muy flexible, y permite introducir código personalizado para la validación de formato numérico y comprobación de rangos, lo que nos brinda la posibilidad de solucionar nuestro problema de forma muy limpia.El siguiente código sería una primera aproximación a la solución del problema. Como podéis observar, simplemente introducimos en
$.validator.methods.number
y $.validator.methods.range
las funciones que queremos utilizar para validar respectivamente los números y los rangos, reemplazando la coma por el punto antes de realizar la conversión con parseFloat()
:<script type="text/javascript">
$.validator.methods.number = function (value, element) {
value = floatValue(value);
return this.optional(element) || !isNaN(value);
}
$.validator.methods.range = function (value, element, param) {
value = floatValue(value);
return this.optional(element) || (value >= param[0] && value <= param[1]);
}
function floatValue(value) {
return parseFloat(value.replace(",", "."));
}
</script>
Si incluimos este script en la página cuando la cultura activa sea la nuestra (o cualquier otra que también utilice la coma para separar decimales), tendremos el problema solucionado.
Una fórmula más elegante y universal sería modificar la función
floatValue()
, y en lugar de reemplazar de forma manual los caracteres, utilizar el plugin Global para realizar la conversión a flotante según la cultura actual. Los detalles de esto, sin embargo, los dejo para otro post.En fin, que como habréis comprobado existen mil y un enfoques posibles para enfrentarnos al problema. Espero que las ideas que hemos ido comentando os sean de utilidad para implementar vuestras propias soluciones hasta que tengamos una vía “oficial” para conseguirlo.
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 11:58 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, jquery, localizacion, trucos, validadores
En este post vamos a ver cómo implementar un formulario con auto-salvado, es decir, capaz de ir enviado su contenido al servidor periódicamente con objeto de evitar la pérdida de toda la información si el usuario sale de forma involuntaria de la aplicación o se pierde la conexión.
Y todo ello de forma no intrusiva, sin afectar a las funcionalidades habituales en casos en que el usuario haya deshabilitado los scripts en el navegador… ¿qué más podemos pedir? ;-)
1. Objetivo
Vamos a empezar por el final, para que quede claro a dónde pretendemos llegar. Implementaremos un mantenimiento para la gestión simple de posts de un motor de blogs, para lo que nos basaremos en las vistas y controladores generados automáticamente por Visual Studio.La siguiente captura muestra el interfaz principal del mantenimiento; seguro que apreciáis la originalidad del diseño ;-)
Al editar o crear un post accederemos a la pantalla de edición, en la que pretendemos crear la funcionalidad de autoguardado, que se puede ver en ejecución en la siguiente captura:
Mientras el usuario esté introduciendo los datos solicitados por el formulario, el sistema de auto-guardado irá activándose cada X segundos, enviando al servidor los datos presentes en los campos del formulario en ese momento. Eso sí, el usuario en ningún momento será interrumpido, puesto que este envío se realizará en segundo plano, mediante una discreta llamada Ajax.
Así, si se produce algún problema como el cierre involuntario del navegador, la salida de la aplicación, o simplemente al usuario se le corta el suministro eléctrico, se podrá recuperar la información enviada en el último salvado automático.
2. La solución, a vista de pájaro
La implementación del autoguardado atenderá estructuralmente al patrón MVC, por lo que:- implementaremos en el Modelo la lógica de recuperación y actualización de datos. Utilizaremos una clase de servicio en la que expondremos los métodos de manipulación de información que vamos a permitir; para el acceso a datos utilizaremos Entity Framework.
- el controlador dispondrá de una acción específica para salvar los datos enviados desde la vista.
- la vista incluirá los scripts que realizan la invocación periódica de la acción del controlador vía Ajax, suministrándole la información disponible en el formulario.
3. El Modelo
Nuestro modelo es bastante simple, puesto que estamos centrándonos exclusivamente en las funcionalidades que necesitamos para implementar el ejemplo de autoguardado.El diagrama del modelo conceptual de Entity Framework es el que se muestra adjunto. Una única entidad,
Post
, con escasas propiedades para no distraernos mucho de nuestro objetivo final.Hemos creado también la correspondiente clase de metadatos con la que aportaremos información necesaria para la validación de la información:
[MetadataType(typeof(PostMetadata))]
public partial class Post
{
}
public class PostMetadata
{
[DisplayName("Título")]
[Required]
public string Titulo { get; set; }
[DisplayName("Texto")]
public string Texto { get; set; }
}
La clase de servicio que implementa las funcionalidades de alto nivel del modelo se denomina
BlogServices
, con la siguiente estructura:public class BlogServices: IDisposable
{
public IEnumerable<Post> GetPosts() { }
public Post GetPostById(int id) { }
public void Save(Post post) { }
public bool DeletePost(int id) { }
public void Dispose() {}
}
El método
Save()
es el único que merece la pena destacar. Su objetivo es comprobar si existe el post, para lo cual nos basamos en la propiedad IdPost
: si vale cero, asumiremos que el post es de nueva creación, por lo que lo insertaremos en la base de datos; en caso contrario, adjuntaremos la entidad al contexto y la marcamos como modificada para que sea actualizada al confirmar los cambios:public void Save(Post post)
{
post.Fecha = DateTime.Now;
if (post.PostId == 0)
{
post.Fecha = DateTime.Now;
_data.AddToPosts(post);
}
else
{
_data.Posts.Attach(post);
_data.ObjectStateManager.ChangeObjectState(post, EntityState.Modified);
}
_data.SaveChanges();
}
Por simplificar el ejemplo, no se han tenido en cuenta los posibles errores de concurrencia.
4. El Controlador
El controlador que podréis encontrar en el código fuente del proyecto incluye las acciones habituales para crear, actualizar, eliminar y obtener la relación de posts almacenados en el sistema.public class PostsController : Controller
{
private BlogServices services = new BlogServices();
[HttpGet] public ActionResult Index() { ... }
[HttpGet] public ActionResult Edit(int id) { ... }
[HttpPost] public ActionResult Edit(Post post) { ... }
[HttpGet] public ActionResult Create() { ... }
[HttpPost] public ActionResult Create(Post post) { ... }
[HttpGet] public ActionResult Delete(int id) { ... }
[HttpPost] public JsonResult SavePostAjax(Post post) { ... }
}
El último método de acción,
SavePostAjax()
, es el que recibirá los datos desde la vista, y su implementación es la mostrada a continuación:[HttpPost]
public JsonResult SavePostAjax(Post post)
{
if (this.ModelState.IsValid)
{
services.Save(post);
Thread.Sleep(2000); // Un retardo
return Json(new
{
grabado = true,
Id = post.PostId
}
);
}
else
{
return Json(new
{
grabado = false
});
}
}
Como se puede observar, tras comprobar la validez de los datos recibidos, invocamos al método
Save()
del objeto services
, una instancia de la clase BlogService
descrita en el Modelo y cuya implementación hemos visto anteriormente, que grabará los datos en el almacén. El método retorna un objeto anónimo serializado en JSON, que presenta como máximo dos propiedades:
grabado
, que indicará si el Post
ha sido almacenado con éxito, e Id
, que retornará el identificador asignado al Post
cuando haya sido salvado por primera vez.La llamada a
Thread.Sleep()
hace que la ejecución quede en pausa un par de segundos, para que cuando estamos en local al menos nos de tiempo a ver los mensajes en pantalla. Obviamente, este código no debería aparecer en producción.5. La Vista
En la vista es donde se encuentra la mayor parte de la magia del autosalvado. Obviamente, estas funcionalidades estarán basadas en scripts, por lo que nos apoyaremos bastante en la inigualable jQuery.El formulario de edición es totalmente normal, como podría una vista tipada de edición generada por Visual Studio, lo que asegurará la compatibilidad con clientes sin scripts. Será idéntico para el interfaz de edición y de creación:
Fijaos que estamos dejando un campo oculto para almacenar el
PostId
del Post
que estamos editando. Cuando estemos creando un post, éste campo almacenará el valor cero, pero una vez el proceso automático envíe por primera vez sus datos, introduciremos en su interior el ID asignado desde la base de datos; así, el siguiente autosalvado, ya no realizará una inserción sobre la base de datos, sino una actualización :-)El siguiente código script muestra la forma en que activamos el guardado automático sobre el formulario de edición:
<script type="text/javascript">
$(function () {
backgroundSaver('<%= Url.Action("SavePostAjax")%>', 15000, getData, setId);
});
function getData() {
if ($.trim($("#Texto").val()) == "")
return null;
else
return {
PostId: $("#PostId").val(),
Titulo: $("#Titulo").val() || "No definido",
Texto: $("#Texto").val()
};
}
function setId(id) {
$("#PostId").val(id);
}
</script>
Al terminar la carga de la página invocamos a la función
backgroundSaver()
, cuyo código veremos más adelante, pasándole los siguientes parámetros:- en primer lugar, el nombre de la acción que recibirá los datos, obtenida con el helper
Url.Action()
, - a continuación el número de milisegundos entre guardado y guardado de datos; en el ejemplo anterior, los datos serán enviados cada 15 segundos,
- seguidamente, la función que utilizará
backgroundSaver()
para obtener los datos a enviar al servidor,getData()
, - y por último, la función mediante la cual
backgroundSaver()
nos informará del identificador asignado al post cuando éste haya sido guardado por primera vez,setId()
.
getData()
debe retornar un objeto anónimo con los datos que queremos enviar al servidor. En nuestro caso, dado que se trata de una instancia de Post, definimos en el objeto anónimo las propiedades PostId
, Titulo
y Texto
, cuyos valores tomamos de los campos del formulario. En el título, además, introducimos un valor por defecto para cuando éste no haya sido definido aún.Observad que hemos introducido una condición previa: si el usuario no ha introducido nada aún en el campo
Texto
, retornamos un nulo, lo que provocará que backgroundSaver()
no efectúe la llamada Ajax al servidor. Sería muy sencillo introducir aquí otras condiciones para impedir peticiones Ajax en momentos en los que no tenga sentido. Por ejemplo, podríamos crear un flag global que indicara si el valor de los campos han cambiado (utilizando los eventos de cambio o pulsación de teclas de los controles), y sólo permitir la llamada cuando dicho indicador fuera cierto.
La función
setId()
será invocada por el proceso de autoguardado cuando haya conseguido almacenar los datos, enviándonos como argumento el identificador asignado al Post
por la base de datos. Lo único que hacemos con ese valor es almacenarlo en el campo oculto que habíamos dispuesto para este propósito. Ya la siguiente vez que guardemos el
Post
, bien sea de forma manual pulsando sobre el botón "Almacenar" del formulario, o bien de forma automática mediante el autosalvado, la información será actualizada en la base de datos y no se insertará un nuevo registro.La función
backgroundSaver()
también es bastante simple. Internamente define dos métodos:prepareNext()
, que programa el siguiente autosalvado,saveForm()
, que es la que lanza la petición Ajax al servidor.
var backgroundSaver = function (action, timer, getData, setId) {
var saveForm = function () {
var data = getData();
if (data == null) { // Cancelamos el envío
prepareNext();
return;
}
$("#autosaving").show();
$.ajax({
url: action,
type: 'post',
cache: false,
data: data,
success: function (retorno) {
$("#autosaving").hide();
if (retorno.grabado) {
if (typeof (setId) == 'function') {
setId(retorno.Id);
}
}
prepareNext();
},
error: function (d) {
$("#autosaving").hide();
prepareNext();
}
});
}
var prepareNext = function () {
setTimeout(saveForm, timer);
};
prepareNext();
}
El cuerpo de la función
saveForm()
contiene la llamada Ajax al servidor. Tras comprobar que la llamada a la función callback getData()
no ha retornado un nulo, se realiza la llamada utilizando el método ajax()
de jQuery. Si la petición se completa con éxito, se invoca a setId()
para notificar al código cliente del ID asignado al Post
.Si en el formulario existe un elemento visual llamado #autosaving, la función anterior se encargará de mostrarlo y ocultarlo cuando corresponda, a modo de indicador de progreso. En nuestro caso, las vistas incluyen un marcado como el siguiente:
6. ¡Y eso es todo!
A pesar de la extensión del este post, implementar esta característica es realmente sencillo, y aporta una espectacularidad impresionante a nuestros formularios, muy en la línea de las modernas aplicaciones 2.0, y por supuesto, grandes ventajas para los usuarios.Obviamente la implementación descrita no es perfecta; podría ser mejorada en mucho añadiendo más automatismos, como la detección automática de cambios en el formulario, o el control de errores, pero no era ese el objetivo que pretendía, así que os lo dejo de deberes ;-)
Podéis descargar el proyecto de demostración, para VS2010 y SQL Express 2008.
Publicado en: Variable not found.