Compartir a través de


Tecnología de vanguardia

Captura predictiva con jQuery y la biblioteca Ajax de ASP.NET

Dino Esposito

El mes pasado, discutí la implementación de vistas principales detalladas mediante el uso de características nuevas que vienen en la biblioteca Ajax de ASP.NET. La lista de características nuevas incluye una sintaxis para enlace de datos cliente en directo y un componente de representación enriquecido, ejemplificado por el control cliente DataView. Al juntar estas características, puede compilar fácilmente vistas anidadas para representar relaciones de uno a muchos datos.

En la biblioteca Ajax de ASP.NET, la mecánica de las vistas principales detalladas se definen ampliamente en la lógica del componente DataView y en la manera en que éste administra y expone sus eventos.

Este mes, daré un paso más y analizaré cómo implementar un patrón de diseño AJAX común y popular (captura predictiva) sobre la biblioteca Ajax de ASP.NET. Básicamente, ampliaré el ejemplo del mes pasado (una vista en profundidad relativamente estándar de los detalles de los clientes) para descargar y mostrar de manera automática y asincrónica pedidos relacionados, si existe alguno. Al hacerlo, hablaré un poco acerca de jQuery y echaré un vistazo a la nueva API de integración de jQuery en la biblioteca Ajax de ASP.NET. Sin más rodeos, revisemos el contexto y compilemos una primera versión del ejemplo.

La demostración por ampliar

En la Figura 1 se muestra el escenario de aplicación sobre el cual agregaré las capacidades de captura predictiva.

imagen: etapa inicial de la aplicación de muestra

Figura 1 Etapa inicial de la aplicación de muestra

La barra de menú permite que el usuario filtre a los clientes por sus iniciales. Una vez hecha la selección, aparece una lista más pequeña de clientes a través de una lista con viñetas en HTML. Ésta es la vista principal.

Cada elemento representado se ha hecho seleccionable. Al hacer clic en uno, aparecen los detalles del cliente en la vista detallada adyacente. Aquí es donde me quedé el mes pasado. Tal como puede ver en la Figura 1, la interfaz de usuario ahora muestra un botón para ver los pedidos. Procedo a partir de aquí en adelante.

La primera decisión que hay que tomar es a nivel de arquitectura y dice relación con el caso de uso que está considerando. ¿Cómo cargaría la información de pedido? ¿Está la información que ya descargó junto con la información de los clientes? ¿Están los pedidos adjuntos a los clientes? ¿Es la carga diferida una opción aquí?

Se espera que el código que estamos considerando se ejecute a nivel cliente, de manera que no pueda confiar en instalaciones de carga diferida compiladas en algunas herramientas de modelado relacional de objetos (O/RM) como Entity Framework o NHibernate. Si los pedidos se van a cargar de manera diferida, entonces cualquier código depende de usted. Por otra parte, si puede suponer que los pedidos ya están disponibles a nivel cliente (es decir, se descargaron junto con la información de los clientes), está casi listo. Todo lo que tiene que hacer es enlazar datos de pedidos a alguna plantilla de HTML y listo.

Obviamente, la cosa se pone mucho más interesante si lo que desea es una carga diferida. Veamos este escenario, entonces.

Como nota al margen, debe saber que la carga diferida es totalmente compatible si obtiene los datos a través del objeto AdoNetDataContext. (Cubriré este tema en artículos a futuro.) Para obtener más información, asegúrese de mirar en asp.net/ajaxlibrary/Reference.Sys-Data-AdoNetServiceProxy-fetchDeferredProperty-Method.ashx.

Una manera nueva de cargar las bibliotecas de scripts

Durante años, los desarrolladores web han quedado a la deriva para descifrar qué archivos de script necesitaría una página. No era una tarea muy complicada, porque la cantidad limitada de código JavaScript sencillo que se usaba facilitaba bastante comprobar si faltaba un archivo requerido. El incremento en las cantidades de código JavaScript complicado en las páginas web introdujo el problema de separar scripts en distintos archivos y, en consecuencia, referirse a ellos correctamente para evitar molestos errores de “objeto indefinido” en tiempo de ejecución.

Muchas bibliotecas populares de JavaScript han estado proporcionando instalaciones a este respecto desde hace años. Por ejemplo, la biblioteca de interfaz de usuario de jQuery tiene un diseño modular y le permite descargar y vincular únicamente las partes que necesita. Esta misma capacidad también la ofrecen los scripts que conforman la biblioteca Ajax de ASP.NET. El cargador de scripts, sin embargo, es algo más.

El cargador de scripts proporciona varios servicios adicionales y se basa en la partición de grandes bibliotecas de scripts en partes más pequeñas. Una vez que dice al cargador las bibliotecas que le interesan, le delega cualquier tarea relacionada con el orden correcto de los archivos requeridos. El cargador de scripts carga todos los scripts requeridos en paralelo y luego los ejecuta en el orden correcto. De esta manera, lo salva de cualquier excepción de “objeto perdido” y proporciona la manera más rápida de administrar los scripts. Todo lo que necesita es hacer una lista de los scripts que desea.

Oiga, espere un momento. Si tengo que enumerar todos los scripts que necesito, ¿cuáles son las ventajas de usar un cargador? Bien, lo que el cargador requiere ni siquiera se acerca a aquello que se requiere en el conocido proceso de vincular ensamblados a un proyecto. Vincule el ensamblado A y deje que el cargador de Visual Studio 2008 descifre cualquier dependencia estática. Éste es un fragmento de código que muestra cómo manejar el cargador de scripts:

Sys.require([Sys.components.dataView, Sys.scripts.jQuery]);

El método Sys.require lleva una matriz de referencias a los scripts que desea vincular a su página. En el ejemplo anterior, su instrucción al cargador es que se ocupe de dos scripts: dataView y jQuery.

Como puede ver, sin embargo, la llamada hecha al método Sys.require no incluye ninguna ruta del servidor web a ningún archivo .js físico. ¿Dónde está la ruta, entonces?

Se requiere que los scripts que trabajarán con el cargador de la biblioteca Ajax de ASP.NET se definan ante el cargador y le informen cuando se cargan por completo. Registrar un script con el cargador no provoca ninguna ida y vuelta, sino que simplemente es una manera de informarle que se le puede llamar para que administre un nuevo script. En la Figura 2 se incluye un extracto de MicrosoftAjax.js que muestra cómo jQuery y jQuery.Validate se registran con el cargador.

Figura 2 Registrar jQuery y jQuery.Validate con el cargador de scripts

loader.defineScripts(null, [
     { name: "jQuery",
       releaseUrl: ajaxPath + "jquery/jquery-1.3.2.min.js",
       debugUrl: ajaxPath + "jquery/jquery-1.3.2.js",
       isLoaded: !!window.jQuery
     },
     { name: "jQueryValidate",
       releaseUrl: ajaxPath + 
                            "jquery.validate/1.5.5/jquery.validate.min.js",
       debugUrl: ajaxPath + "jquery.validate/1.5.5/jquery.validate.js",
       dependencies: ["jQuery"],
       isLoaded: !!(window.jQuery && jQuery.fn.validate)
     }
    ]);

Desde luego, puede usar este enfoque con scripts personalizados y controles cliente también. En ese caso, necesita hacer referencia a la definición específica del cargador del script además de su script verdadero. Una definición específica del cargador incluye rutas de servidor de liberación y depuración del script, un nombre público usado para hacer referencia a él, dependencias y una expresión por evaluarse a fin de probar si la biblioteca se cargó correctamente.

Para usar el componente del cargador de scripts, tiene que hacer referencia a un nuevo archivo JavaScript denominado start.js. Éste es un extracto de la aplicación de muestra que usa una mezcla de técnicas antiguas y nuevas de carga de scripts.

<asp:ScriptManagerProxy runat="server" ID="ScriptManagerProxy1">
    <Scripts>
        <asp:ScriptReference Path="~/Scripts/Ajax40/Preview6/start.js"/>
        <asp:ScriptReference Name="MicrosoftAjax.js" 
                       Path="~/Scripts/MicrosoftAjax.js"/>
        <asp:ScriptReference Path=
                                 "~/Scripts/MicrosoftAjaxTemplates.js"/>
     <asp:ScriptReference Path="~/MasterDetail4.aspx.js"/>
   </Scripts>
</asp:ScriptManagerProxy>

Haga referencia al archivo start.js mediante un elemento de <script> clásico. Se puede hacer referencia a otros scripts mediante el uso del control ScriptManager, elementos simples de <script> o el método Sys.require. Como puede ver a partir del fragmento de código anterior, no hay ninguna referencia a la biblioteca de jQuery. De hecho, se hace referencia a la biblioteca de jQuery de manera programática desde el archivo JavaScript específico de la página vinculado mediante ScriptManager.

Otra característica interesante de la biblioteca Ajax de ASP.NET es la disponibilidad de las características de jQuery a través del espacio de nombres Sys y, por el contrario, la exposición de componentes cliente de Microsoft como los complementos de jQuery. Esto significa, por ejemplo, que puede registrar un controlador de eventos para el evento listo (una tarea típica de jQuery) mediante la función Sys.onReady, tal como se muestra aquí.

Sys.onReady(
    function() {
        alert("Ready...");
    }
);

Dadas todas estas características nuevas, el inicio típico de un archivo JavaScript que se va a usar como extensión de una página web se ve así:

// Reference external JavaScript files

Sys.require([Sys.scripts.MicrosoftAjax, 

             Sys.scripts.Templates, 

             Sys.scripts.jQuery]);
Sys.onReady(
    function() {
        // Initialize scriptable elements 

        // of the page.
    }
);

Sin embargo, es posible un enfoque incluso más sencillo. Puede usar Sys.require para cargar un control como DataView en lugar de los archivos que implementa. El cargador de scripts cargaría estos archivos de manera automática basado en las dependencias definidas para DataView. Centrémonos en la captura predictiva.

Control de la selección de los clientes

Para obtener la interfaz de usuario que se muestra en la Figura 1, usa plantillas HTML y adjunta datos a marcadores de posición enlazados a datos mediante el componente DataView. Los detalles de los clientes se muestran de manera automática debido al enlace de daros basado en DataView cuando hace clic en un cliente de la lista. Los pedidos, sin embargo, no se enlazan directamente mediante DataView. Esto se debe a los requisitos que establecimos al principio del artículo: por diseño, los pedidos no se descargan con la información de los clientes.

Para capturar los pedidos, por tanto, tiene que controlar el cambio de la selección dentro de la plantilla asociada con DataView. Actualmente, DataView no origina un evento cambiado por selección. DataView sí proporciona un excelente apoyo para los escenarios principales detallados, pero gran parte de eso ocurre de manera automática, aun cuando puede crear comandos y controladores personalizados (consulte asp.net/ajaxlibrary/Reference.Sys-UI-DataView-onCommand-Method.ashx). En particular, configura el atributo sys:command para “seleccionar” cualquier elemento en que se puede hacer clic que pueda originar la vista en detalle, tal como se muestra aquí:

<li>
   <span sys:command="Select" 
         id="itemCustomer" 

         class="normalitem">
   <span>{binding CompanyName}</span>
   <span>{binding Country}</span>
   </span>        
</li>

Cuando se hace clic en el elemento, desencadena un evento onCommand al interior de DataView y, como resultado, el contenido de la propiedad selectedData se actualiza para reflejar la selección. En consecuencia, se actualiza cualquier parte de la plantilla enlazada a selectedData. El enlace de datos, sin embargo, implica la actualización de datos exhibidos, pero no la ejecución de ningún código.

Tal como se mencionó cuando se desencadena un comando al interior de DataView, el evento onCommand se genera internamente. En su calidad de desarrollador, usted puede registrar su propio controlador para el evento. Lamentablemente, al menos con la actual versión preliminar del componente DataView, el controlador de comandos se invoca antes de que se actualice la propiedad de índice seleccionada. El efecto neto es que puede interceptar cuando se está a punto de mostrar la vista detallada, pero no tiene idea acerca del contenido nuevo en exhibición. El único objetivo del evento parece ser otorgar a los desarrolladores una manera de evitar el cambio de selección, en caso de que no se verifiquen ciertas condiciones críticas.

Un enfoque que funciona actualmente, y que seguirá funcionando en el futuro independientemente de cualquier mejora en el componente DataView, es el siguiente. Adjunte un controlador “onclick” a cualquier elemento de la vista principal en que se pueda hacer clic y enlace un atributo adicional para contener cualquier información clave que sea útil. Éste es el nuevo marcado para la porción repetible de la vista principal:

<li>
   <span sys:command="Select" 

         sys:commandargument="{binding ID}"

         onclick="fetchOrders(this)"
         id="itemCustomer" 

         class="normalitem">
   <span>{binding CompanyName}</span>
   <span>{binding Country}</span>
   </span>        
</li>

El marcado presenta dos cambios. Primero, ahora incluye un nuevo atributo sys:commandargument; segundo, tiene un controlador para el evento de clic. El atributo sys:commandargument contiene el Id. del cliente que se seleccionó. El Id. se emite a través de un enlace de datos. El atributo donde deja el Id. no tiene que ser sys:commandargument necesariamente; también puede usar cualquier atributo personalizado.

El controlador de clics es responsable de capturar los pedidos de acuerdo con cualquier directiva de carga que haya establecido. En la Figura 3 se muestra el código fuente del cargador de pedidos.

Figura 3 Código para capturar pedidos

function fetchOrders(elem)
{
    // Set the customer ID
    var id = elem["commandargument"];
    currentCustomer = id;
    
    // Check the jQuery cache first
    var cachedInfo = $('#viewOfCustomers').data(id);
    if (typeof (cachedInfo) !== 'undefined') 
        return;

    // Download orders asynchronously
    $.ajax({
         type: "POST",
         url: "/mydataservice.asmx/FindOrders",
         data: "id=" + id,
         success: function(response) {

              var output = response.text;
              $('#viewOfCustomers').data(id, output);
              if (id == currentCustomer)
                  $("#listOfOrders0").html(output);
         }
    });
}

La función fetchOrders recibe el elemento DOM en que se hizo clic. Primero, recupera el valor del tributo acordado que contiene el Id. del cliente. A continuación, comprueba si los pedidos ya existen en la memoria caché cliente de jQuery. Si no es así, finalmente procede a realizar una descarga asincrónica. Usa un método jQuery AJAX para organizar una solicitud POST a un servicio web. Estoy suponiendo en este ejemplo que el servicio web emplea el patrón AJAX “Mensaje HTML” y devuelve HTML sin formato listo para fusionarse con la página. (Tenga en cuenta que éste no es necesariamente el mejor enfoque y funciona mayoritariamente en escenarios heredados. Desde una perspectiva de diseño puro, una consulta de un endpoint para datos JSON generaría una carga mucho más liviana.)

Si la solicitud se realiza correctamente, el marcado de pedidos primero se almacena en la memoria caché y luego se muestra donde debe ir (ver Figura 4).

imagen: Captura y exhibición de pedidos

Figura 4 Captura y exhibición de pedidos

En la Figura 4 se muestra una captura de pantalla y realmente no se explica qué está pasando. Cuando hace clic para ver los detalles de un cliente, la solicitud de pedidos se desencadena de manera asincrónica. Entretanto, se muestran los detalles del cliente. Como podrá recordar, no hay necesidad de descargar la información del cliente a pedido, ya que dicha información se descarga en fragmentos cuando el usuario hace clic en el menú de iniciales de alto nivel.

Es posible que la descarga de pedidos demore un poco y es una operación que no entrega (o requiere) ningún comentario hacia el usuario. Tan sólo sucede y es un proceso completamente transparente para el usuario. La idea del patrón de captura predictiva es que capture información de antemano que el usuario posiblemente solicite. Para representar una verdadera ventaja, esta característica debe implementarse de manera asincrónica y, desde una perspectiva de capacidad de uso, es preferible que sea invisible para el usuario.

Centrémonos en las tareas más comunes que un usuario realizaría en la interfaz de usuario en la Figura 4. Un usuario normalmente haría clic para seleccionar a un cliente. A continuación, el usuario probablemente pasaría unos momentos leyendo la información en pantalla. Mientras el usuario ve la pantalla, los pedidos para el cliente seleccionado se descargan silenciosamente.

Es posible que el usuario solicite, o no, ver los pedidos de inmediato. Por ejemplo, es posible que el usuario decida cambiar a otro cliente, leer la información y luego retornar al primero, o quizás navegar hacia otro. En cualquier caso, con sólo hacer clic para ver los detalles de un cliente, el usuario desencadena la captura de pedidos relacionados.

¿Qué pasa con los pedidos descargados? ¿Cuál sería la manera recomendada de tratarlos durante la descarga?

Cómo tratar los pedidos capturados

Para ser franco, no encuentro una manera claramente preferida de tratar los datos cargados previamente en semejante escenario. Principalmente depende de la opinión que obtenga de las partes interesadas y los usuarios finales.

Sin embargo, sugeriría que los pedidos se muestren de manera automática si el usuario aún está viendo al cliente para el cual la descarga de pedidos se acaba de completar.

El método $.ajax funciona de manera asincrónica y se adjunta a su propia devolución de llamada correcta. La devolución de llamada recibe pedidos descargados para un cliente dado, pero al momento en que se ejecuta, es posible que el cliente en pantalla sea diferente. La directiva que usé garantiza que los pedidos se muestren directamente si hacen referencia al cliente actual. De lo contrario, los pedidos se almacenan en la memoria caché y quedan disponibles para cuando el usuario vuelve y hace clic en el botón “Ver pedidos”.

Echemos un segundo vistazo a la devolución de llamada correcta para el procedimiento de captura:

function(response) 
{
  // Store orders to the cache

  $('#viewOfCustomers').data(id, response.text);

  // If the current customer is the customer for which orders

  // have been fetched, update the user interface 

  if (id == currentCustomer)
     $("#listOfOrders0").html(response.text);
}

La variable id es local respecto del método $.ajax y se configura con el Id. del cliente para el cual se capturan los pedidos. La variable currentCustomer, sin embargo, es una variable global que se configura en cualquier momento en que se ejecuta el procedimiento de captura (ver Figura 3). El truco es que una variable global se puede actualizar desde varios puntos, así que la comprobación al final de la devolución de llamada tiene sentido.

¿Cuál es la función del botón “Ver pedidos” que ve en la Figura 1 y la Figura 4? El botón está allí para los usuarios que desean ver los pedidos para un cliente dado. Por diseño, en este ejemplo mostrar los pedidos es una opción. De allí que sea un elemento razonable tener un botón que desencadena la vista en la interfaz de usuario.

Cuando el usuario hace clic en ver los pedidos, es posible, o tal vez no, que haya información disponible en ese momento. Si los pedidos no están disponibles, significa que (por diseño) la descarga está pendiente o que ha habido un error por algún motivo. Por lo tanto, al usuario se le presenta la interfaz de usuario que aparece en la Figura 5.

imagen: Los pedidos aún no están disponibles

Figura 5 Los pedidos aún no están disponibles

Si el usuario permanece en la misma página, los pedidos se muestran automáticamente cuando la descarga finaliza correctamente, como en la Figura 4.

El primer cliente en pantalla

Falta algo para finalizar la demostración de este escenario principal detallado enriquecido con capacidades de captura predictiva. El componente DataView permite que la especificación de un elemento de datos en particular se represente en el modo seleccionado en pantalla. Usted controla el elemento para que se seleccione inicialmente mediante el atributo initialselectedindex del componente DataView. Lo podemos ver a continuación:

<ul class="sys-template" sys:attach="dataview" id="masterView"
    dataview:dataprovider="/aspnetajax4/mydataservice.asmx"
    dataview:fetchoperation="LookupCustomers"
    dataview:selecteditemclass="selecteditem" 
    dataview:initialselectedindex="0">

En este caso, el primer cliente recuperado para la inicial seleccionada se muestra automáticamente. Como el usuario no necesita hacer clic, no se produce ninguna captura automática de pedidos. Todavía puede obtener acceso a los pedidos del primer cliente si hace clic en él nuevamente. De esta manera, el primer cliente se procesará como cualquier otro cliente en pantalla. ¿Hay alguna manera de evitar este comportamiento?

En el caso del usuario, hacer clic para ver los pedidos no sería suficiente para el primer cliente. De hecho, el controlador de botones limita la información que se puede mostrar para lo que hay en la memoria caché. Esto se hace para evitar un comportamiento duplicado en código y para tratar de hacer todo únicamente una sola vez. A continuación, podemos ver:

function display() 
{
    // Attempt to retrieve orders from cache
    var cachedInfo = $('#viewOfCustomers').data(currentCustomer);
    if (typeof (cachedInfo) !== 'undefined')  
        data = cachedInfo;
    else
        data = "No orders found yet. Please wait ...";

    // Display any data that has been retrieved
    $("#listOfOrders0").html(data);
}

La función de exhibición anterior tiene que mejorarse levemente para desencadenar capturas de pedidos en el caso de que no se haya seleccionado ningún cliente actualmente. Éste es otro motivo para tener una variable global currentCustomer. Éste es el código editado de la función de exhibición:

function display() 
{
    if (currentCustomer == "") 
    {
        // Get the ID of the first item rendered by the DataView
        currentCustomer = $("#itemCustomer0").attr("commandargument");

        // The fetchOrders method requires a DOM element.
        // Extract the DOM element from the jQuery result.
        fetchOrders($("#itemCustomer0")[0]);    
    }

    // Attempt to retrieve orders from cache
    ...

    // Display any data that has been retrieved
    ...
}

Si no se ha seleccionado ningún cliente de manera manual, se lee el atributo sys:commandargument del primer elemento representado. La manera más rápida de hacerlo es aprovechar la convención de nombres para el Id. de elementos representados a través de DataView. El Id. original se anexa con un número progresivo. Si el elemento Id. original es itemCustomer, entonces el Id. del primer elemento será itemCustomer0. (Éste es un aspecto de DataView que puede cambiar en la versión final de la biblioteca Ajax de ASP.NET.) Además tenga en cuenta que fetchOrders requiere que entregue un elemento DOM. Una consulta jQuery devuelve una colección de elementos DOM. Es por ello que en el código anterior debe agregar un selector de elementos.

Por último, tenga en cuenta que otra solución también es posible, si le parece aceptable que ningún cliente se muestre inicialmente después del enlace de datos. Si configura el atributo initialselectedindex de DataView en -1, entonces no se seleccionará ningún cliente inicialmente. Como resultado, para ver los detalles de los pedidos, tiene que hacer clic en cualquier cliente, lo cual desencadenaría la captura de pedidos asociados.

Conclusión

DataView es un instrumento formidable para el enlace de datos en el contexto de aplicaciones web cliente. Está diseñado específicamente para escenarios comunes, tales como las vistas principales detalladas. Sin embargo, no admite todos los escenarios posibles. En este artículo, mostré algún código que amplía una solución de DataView a través de la implementación del patrón de “captura predictiva”.

[La biblioteca Ajax de ASP.NET beta está disponible para descarga en ajax.codeplex.com*. Se espera que salga al mercado al mismo tiempo que Visual Studio 2010.—Ed.*]

Dino Esposito es el autor del próximo libro “Programming ASP.NET MVC” de Microsoft Press y coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Con residencia en Italia, Esposito participa habitualmente en conferencias y eventos del sector en todo el mundo. Puede participar en su blog en weblogs.asp.net/despos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Stephen Walther