Autor en Google+
Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

10 años online 🎂

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
miércoles, 15 de septiembre de 2010
ASP.NET MVC, por su natural integración con Ajax, es un candidato perfecto para implementar software al “estilo 2.0”, consiguiendo efectos sorprendentes y nuevas formas de interacción con el usuario desde la web similares a las que nos ofrecen aplicaciones como GMail o Blogger.

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 ;-)

Pantalla principal del mantenimientoAl 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:

Autoguardado en funcionamiento
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.
Al final del post encontrarás un enlace para descargar la solución completa. En ella podrás ver otras técnicas, que no comento por aquí para no alargar demasiado este texto, como la eliminación de posts usando también Ajax no intrusivo, o el uso de plantillas y templated helpers para la reutilización del interfaz de edición.

3. El Modelo

Modelo conceptualNuestro 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:

Código del formulario de edició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().
La función 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.
Ahí va su código:

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:

Indicador de progreso

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.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

2 Comentarios:

Juan Maria Nieves dijo...

Buen articulo.

Pero creo que el parametro getData
de backgroundSaver() es redundante ya que en saveForm() tienes esta linea var data = getData();

José M. Aguilar dijo...

Hola, Juan María!

Sí, en algunos escenarios podríamos simplificar la función como dices. De hecho, hasta podría evitarse el parámetro setId, por la misma razón.

Sin embargo, el hecho de tenerlo así es por pura "encapsulación". Observa que las llamadas a getData() y a setId() en el interior de saveForm() invocan a las funciones callback que suministramos como argumento, y no a las funciones externas.

Esto hace posible, por ejemplo, que podamos utilizar más de un backgroundSaver() en una misma página, puesto que éste recibe las "dependencias" que necesita para funcionar, es decir, las referencias a estas funciones. O en otras palabras, utilizo una especie de inyección de dependencias casera.

Saludos & gracias mil por aportar!