sábado, 13 de junio de 2015

SlickGrid. Carga asíncrona de celdas (I) - La propiedad asyncPostRender

Artículos anteriores:
Ya hemos visto cómo dar formato y editar las celdas de los grids. Sin embargo hay una cuestión que todavía no hemos abordado: ¿qué sucede cuando la información que tenemos que mostrar en una celda la debemos recuperar del servidor?

En los ejemplos vistos hasta ahora teníamos un campo de denominación de subcategoría, la cual recuperábamos de un array en cliente. En una aplicación normal no podemos (o al menos no debemos por razones obvias de rendimiento) disponer de toda la información de nuestra base de datos en cliente, por lo que deberemos acceder al servidor para recuperarla.

Podríamos modificar la función de formateo de celda para que realice una llamada al servidor y devuelva la misma información, pero esto ralentizaría enormemente la visualización de los grids provocando que el navegador se bloquee hasta que termina de recuperar toda la información.

Por suerte el SlickGrid nos permite definir una función a través de la propiedad asyncPostRender de la columna que se encargue de obtener la información de la celda de forma asíncrona.


Creando el escenario

Para ver hasta qué punto esto puede ser un problema voy a crear un escenario que simule el comportamiento del grid recuperando de forma síncrona la denominación a través de la función de formateo, para poder ver a continuación cómo podemos solucionarlo con la propiedad asyncPostRender.

Para tener más datos de prueba he ampliado el array de productos volcando los 504 artículos de la tabla Product de la base de datos AdventureWorks2012. Como en algunos productos se utilizan otros literales en el campo Color los he añadido a las funciones de formateo y edición de esta columna. No voy a incluir todo este código porque entiendo que no aporta nada al ejemplo y el que lo desee lo tiene disponible en la descarga del código completo de los ejemplos de SlickGrid.

Dentro del arhcivo productdatasource.js voy a crearme también una función getSubcatNameFromBBDD que simule una recuperación de la denominación de la subcategoría desde un servidor a partir del código de la subcategoría. Para ello la función recupera la denominación del array subcategories pero le he añadido un retardo de 200 milisegundos antes de devolver la información a través de una función sleep (es un retardo considerable pero así se verá más claro el problema y, dentro del desarrollo de nuestras aplicaciones, deberíamos tener en cuenta que en algunas circusntancias se puede llegar a dar este tipo de escenarios y nuestra aplicación debería considerarlos).

function getSubcatNameFromBBDD(subcategory) {
    function sleep(milliseconds) {
        var start = new Date().getTime();
        for (var i = 0; i < 1e7; i++) {
            if ((new Date().getTime() - start) > milliseconds) {
                break;
            }
        }
    }
    var name = "";
    for (var subcat in subcategories) {
        if (subcategory == subcategories[subcat].ProductSubcategoryID) {
            name = subcategories[subcat].Name;
            break;
        }
    }
    sleep(200);
    return name;
}

Finalmente voy a reemplazar la función de formateo de la columna de denominación de la subcategoría por una nueva función asyncSubcategoryNameFormatter que utilice la función getSubcatNameFromBBDD para recuperar la información.
Para este ejemplo he creado en el sitio web una nueva página html CargaCeldaAsincrona.html que parte de la que creamos en el ejemplo de edición.

function asyncSubcategoryNameFormatter(row, cell, value, columnDef, dataContext) {
    if (value==null) return "";

    return getSubcatNameFromBBDD(value);
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Carga asíncrona de celdas</title>
    <link href="https://code.jquery.com/ui/1.11.4/themes/blitzer/jquery-ui.css" rel="stylesheet" />
    <link href="css/slick.grid.css" rel="stylesheet" />
    <link href="css/theme/slick-default-theme.css" rel="stylesheet" />
    <link href="css/pildorasgrid.css" rel="stylesheet" />
    <script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
    <script src="http://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
    <script src="Scripts/jquery.event.drag-2.2.js"></script>
    <script src="Scripts/slick.core.js"></script>
    <script src="Scripts/slick.grid.js"></script>
    <script src="Scripts/gridformatters.js"></script>
    <script src="data/productdatasource.js"></script>
    <script src="Scripts/grideditors.js"></script>
    <script type="text/javascript">
        $(function () {
            var columns = [
                { name: "ID", field: "ProductID", id: "ProductID", width: 60, resizable: false
                    , headerCssClass: "prKeyHeadColumn", cssClass: "numericCell", editor: Slick.Editors.Text },
                { name: "Nº Producto", field: "ProductNumber", id: "ProductNumber", width: 120, resizable: false
                    , headerCssClass: "headColumn", editor: Slick.Editors.Text },
                { name: "Denominación", field: "Name", id: "Name", width: 250, minWidth: 150, maxWidth: 400
                    , headerCssClass: "headColumn", editor: Slick.Editors.Text },
                { name: "Color", field: "Color", id: "Color", width: 80, minWidth: 60, maxWidth: 120
                    , headerCssClass: "headColumn", formatter: Slick.Formatters.ColorFormatter
                    , editor: Slick.Editors.Color },
                { name: "Precio", field: "StandardCost", id: "StandardCost", width: 110, minWidth: 80, maxWidth: 170
                    , headerCssClass: "headColumn", cssClass: "numericCell", formatter: Slick.Formatters.CurrencyFormatter
                    , editor: Slick.Editors.Text },
                { name: "Sub", field: "ProductSubcategoryID", id: "ProductSubcategoryID", width: 60, resizable: false
                    , headerCssClass: "headColumn", cssClass: "numericCell", editor: subcategoryEditor },
                { name: "Subcategoría", field: "ProductSubcategoryID", id: "SubcategoryName", width:200, minWidth: 150, maxWidth: 400
                    , headerCssClass: "headColumn", formatter: asyncSubcategoryNameFormatter }
            ];

            var options = {
                editable: true,
                autoEdit: false
            };

            var grid = new Slick.Grid("#AsyncGrid", products, columns, options);
        });
    </script>
</head>
<body>
    <div id="AsyncGrid" style="width:1024px; height:500px;"></div>
</body>
</html>

Si cargamos la página veremos que las primeras filas representan productos que no tienen asociada ninguna subcategoría y se muestran de manera normal. Sin embargo, si hacemos scroll hasta encontrar productos con subcategoría asignada, comprobaremos que el navegador se nos bloquea mientras el grid recupera toda la información a mostrar generando una experiencia de usuario cuando menos bastante lamentable.

Navegador bloqueado

Recuperando la información de manera asíncrona

Para solucionar el problema voy a utilizar, como he comentado antes, una función asignada a la propiedad asyncPostRender de la columna. Esta propiedad nos permite asignar una función que será llamada por el grid una vez visualizadas las filas y a través de un timeout de forma que minimice el impacto en el rendimiento del navegador.

La función asignada a la propiedad asyncPostRender recibirá cuatro parámetros:

  • cellNode: que representa el objeto DOM de la celda
  • row: índice de la fila en la que se encuentra la celda
  • dataContext: objeto de datos de la fila
  • colDef: objeto de definición de la columna

La función no debe devolver ningún valor sino que debe encargarse por sí sola de introducir el contenido a visualizar en el objeto cellNode.

Así que voy a modificar la función de formateo asyncSubcategoryNameFormatter para que no recupera la denominación y se limite a mostrar un literal "Cargando...", y definiré una nueva función getSubcategoryName que asignaré a la propiedad asyncPostRender y que será la responsable de recuperar la información y dibujarla en la celda.

function asyncSubcategoryNameFormatter(row, cell, value, columnDef, dataContext) {
    if (value==null) return "";

    return "Cargando...";
}

function getSubcategoryName(cellNode, row, dataContext, colDef) {
    var cell = $(cellNode);
    if (cell.text() !== "Cargando...") return;

    var value = dataContext[colDef.field];
    var name = getSubcatNameFromBBDD(value);

    cell.text(name);
}

La función getSubcategoryName recupera el código de la subcategoría del objeto dataContext y se lo pasa a la función getSubcatNameFromBBDD para obtener la denominación. Finalmente establece la denominación como texto de la celda.

En la página CargaCeldaAsincrona.html deberemos establecer la función getSubcategoryName como valor de la propiedad asyncPostRender de la columna y modificar el objeto de opciones de configuración del grid para establecer la propiedad del grid enableAsyncPostRender a true, si esta propiedad está desactivada (que lo está por defecto) el SlickGrid ignora cualquier función asignada a las propiedades asyncPostRender de las columnas.

var columns = [
 { name: "ID", field: "ProductID", id: "ProductID", width: 60, resizable: false
  , headerCssClass: "prKeyHeadColumn", cssClass: "numericCell", editor: Slick.Editors.Text },
 { name: "Nº Producto", field: "ProductNumber", id: "ProductNumber", width: 120, resizable: false
  , headerCssClass: "headColumn", editor: Slick.Editors.Text },
 { name: "Denominación", field: "Name", id: "Name", width: 250, minWidth: 150, maxWidth: 400
  , headerCssClass: "headColumn", editor: Slick.Editors.Text },
 { name: "Color", field: "Color", id: "Color", width: 80, minWidth: 60, maxWidth: 120
  , headerCssClass: "headColumn", formatter: Slick.Formatters.ColorFormatter
  , editor: Slick.Editors.Color },
 { name: "Precio", field: "StandardCost", id: "StandardCost", width: 110, minWidth: 80, maxWidth: 170
  , headerCssClass: "headColumn", cssClass: "numericCell", formatter: Slick.Formatters.CurrencyFormatter
  , editor: Slick.Editors.Text },
 { name: "Sub", field: "ProductSubcategoryID", id: "ProductSubcategoryID", width: 60, resizable: false
  , headerCssClass: "headColumn", cssClass: "numericCell", editor: subcategoryEditor },
 { name: "Subcategoría", field: "ProductSubcategoryID", id: "SubcategoryName"
  , width:200, minWidth: 150, maxWidth: 400, headerCssClass: "headColumn"
  , formatter: asyncSubcategoryNameFormatter, asyncPostRender: getSubcategoryName }
];

var options = {
 editable: true,
 autoEdit: false,
 enableAsyncPostRender: true
};

Si recargamos la página veremos que, aunque el comportamiento del grid no sea el ideal: en las celdas de denominación aparece el literal "Cargando..." y tarda en mostrarse el valor definitivo (no debemos olvidar que hemos establecido dos décimas de segundo de retardo en cada petición), al menos el navegador no se bloquea y la experiencia del usuario mejora mucho al poder interactuar con la página mientras las denominaciones acaban de cargarse.

Cargando datos

¿Podemos ir aún más allá?

Si hacemos scroll arriba y abajo por el grid podremos comprobar que, al visualizar filas que ya hemos mostrado con anterioridad, el grid vuelve a mostrar el literal "Cargando..." en la celda y vuelve a recuperar la información de la denominación, con el consecuente retardo.

Pero, ¿qué sentido tiene que volvamos a acceder a nuestro servidor para recuperar una información que ya hemos obtenido anteriormente? Este comportamiento, además de empobrecer la experiencia del usuario, provoca una sobrecarga de peticiones a nuestro servidor.

En el siguiente artículo veremos cómo podemos reducir este problema generando una caché de los datos recuperados.

El código


Puedes descargar el código de todos los ejemplos de SlickGrid de:



Artículo siguiente:
SlickGrid. Carga asíncrona de celdas (y II) - Generando una caché de datos

No hay comentarios:

Publicar un comentario