Síntesis Generalizada de Ajax con Callbacks

 

Una implementación Ajax basada en Callbacks no es simple aun para programadores experimentados, pero disponiendo de una buena estrategia lo es

 

Por Harvey Triana, 12 de enero de 2007

 

Introducción

 

ASP.NET 2.0 suministra la interfaz suficiente para programar Ajax al disponer el mecanismo de callbacks en su lenguaje. No obstante, en “código plano” el esquema es aun complicado y se requiere una visión algo profunda del asunto para ponerlo en real producción. Esto, y otras necesidades de desarrollo, dan origen a modelos de objetos de alto nivel como son ‘Atlas’,  ‘ASP. NET Ajax’, o herramientas de terceros.  No obstante, muchos programadores prefieren evitar la dependencia de estas herramientas sofisticadas,  y al mismo tiempo desean implementar Ajax sin demasiadas tribulaciones.

 

Pues bien en la red encuentras muchos ejemplos de implementar Ajax con Callbacks, pero casi ninguno, o ninguno, muestran el ciclo completo, desde interfaz de usuario hasta manejo de datos, tras una solución real. Una de la pregunta que he visto sin responder, o ambiguamente respuesta, es ¿Cómo recoger los datos  posteriores a un callback? Este artículo responde esta pregunta, y suministra una estrategia eficaz para generalizar Ajax basado en callbacks.

 

Introducción a Ajax con Callbacks

 

Usando callbacks podemos desde el código del servidor leer el resultado de una función o evento de JavaScript. Elaborar una respuesta en el código del servidor, y con este resultado retornar al código del lado del cliente y actualizar la pagina ASPX sin postback. Básicamente, un callback es un dialogo asíncrono entre el código JavaScript de la página y el código .NET del servidor.

 

Para usar un Callback de ASP.NET 2.0 necesitamos usar la siguiente abstracción, siendo la misma para cualquier solución:

 

1.      Implementar la interfaz ICallbackEventHandler

2.      Configurar las llamadas asincronas en Page_Load

3.      Escribir respuesta en RiseCallbackEvent, según su argumento que es enviado desde un evento o función JavaScript

4.      Enviar respuesta desde GetCallbackResult a un método JavaScript, el cual escribe una respuesta dinámica en la página.

 

Sugiero el estudio de la referencia 1 de este artículo si nunca ha trabajado callbacks de ASP.NET 2.0. Realmente este artículo va más allá de la exposición teórica del asunto. Si ha leído ejemplos de Ajax con callbacks quizás tenga la impresión de que es heterogéneo, y que necesitamos mucho JavaScript, y pericia. Pues bien, es posible sintetizar un esquema en pasos o reglas en donde al final solo nos ocuparemos por la respuesta del callback. El objetivo es programar rápidamente Ajax con callbacks sin preocuparnos en demasía por el JavaScript implicado.

 

Un Ejemplo Clásico

 

En casi todos los formularios de registro a un sitio Web encontramos una lista país, y una lista sometida, la cual muestra las divisiones políticas del país. Son datos importantes de los clientes. Ahora, en las aplicaciones sin Ajax, luego de seleccionar el país, el usuario experimenta una recarga de la página para disponer la lista de las divisiones políticas del país que escogió. Este postback muchas veces desconcentra y confunde al usuario, ¿Qué hice? Se pregunta. Cuando programamos la página con Ajax, el postback se suprime, y la experiencia del usuario es cómoda. Suministrar una interfaz Web cómoda es uno de los grandes motivos dio origen a Ajax.

 

En la red encontramos varios ejemplos Ajax de esta típica relación. Algunos usando callbacks. No obstante, no he encontrado uno solo que cumpla el ciclo, es decir, que cuando se de “aceptar”, nos de el mecanismo para recoger los datos actuales del formulario. Si desapercibidamente se hace con un postback, por ejemplo desde un botón Submit, veremos que lo datos se pierden, o en algunos casos obtenemos el siguiente error:

 

“Invalid postback or callback argument.  Event validation is enabled using <pages enableEventValidation="true"/> in configuration or <%@ Page EnableEventValidation="true" %> in a page.  For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them.  If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation”.

 

En ninguna caso se trata de un bug, ni hay que tratar de hacer una reparación de configuración. Es mensaje es natural. Debe asimilarse que una página que se procesa con callbacks nunca debe someterse a un postback. Debe entenderse que tras un callback, la pagina ‘vive’  en la memoria del cliente que visita la página. Como un ejercicio, los invito a que tras un callback, usen el explorador para ver el código fuente de la página, verán que el código de la página es aquel que se cargo por primera vez, y no coincide con los datos que se ven actualmente, puesto que estos yacen en memoria.

 

El propósito de este artículo no es simplemente decir como dar una solución eficaz al ejemplo propuesto, sino como dar una estrategia generalizada para dar soluciones Ajax con callback, manteniendo en su mínima expresión el código JavaScript. Al fin que los que usamos tecnologías .NET somos esencialmente programadores .NET, no JavaScript.

 

De aquí en adelante, voy a perfilar una estrategia a través de un conjunto de reglas para sintetizar Ajax basado en callbacks.

 

 

Los Datos del Ejemplo

 

Los datos de las listas relacionadas “Países” y “Divisiones Políticas”, pueden yacer en cualquier base de datos, archivo XML, o el recurso que Usted prefiera. En el ejemplo, diseñe una simple base de datos en MS Access, cuyo esquema se ve en la siguiente imagen:

 

Base de datos Sample.mdb

 

 

 

Esta base de datos cuyo nombre es Sample.mdb, se copia en la ruta App_Data de la aplicación Web.

 

La Página del Ejemplo

 

La página ASPX con la que expongo el ejemplo la he nombrado CBStrategy.aspx. Solo contiene un titulo, dos listas de tipo DropDownList, y un control de etiqueta:

 

 

Código html de la página CBStrategy.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="CBStrategy.aspx.cs" Inherits="CBStrategy" %>

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

 

<html xmlns="http://www.w3.org/1999/xhtml" >

<head id="Head1" runat="server">

<title>Sintetizando Ajax con Callbacks, Ejemplo</title>

<script type="text/javascript" src="App_Scripts/CallbackComplement.js"></script>

</head>

<body>

    <form id="form1" runat="server">

    <h1>Sintetizando Ajax con Callbacks, Ejemplo</h1>

    <hr />

    <div>

        <!-- Países -->

        <asp:DropDownList ID="ddlPaíses" runat="server" Width="300px" />

        <br /><br />

        <!-- Divisiones por País -->

        <div id="div_ddlDivisiones">

             <asp:DropDownList ID="ddlDivisiones" runat="server" Width="300px" />

        </div>

    </div>

    <hr />

    <br />

    <asp:Button ID="btnEnviar" runat="server" Text="Aceptar"

                UseSubmitBehavior="False" /><br />

    <!-- Mostrar datos seleccionados por el usuario -->

    <div id="div_lblInfo">

        <asp:Label ID="lblInfo" runat="server" Text="" />

    </div>

    </form>

</body>

</html>

 

Observe las etiquetas div_ddlDivisiones,  div_lblInfo, y div_lblInfo las cuales envuelven controles ASP.NET. Estas etiquetas div marcan los controles que se van a modificar dinámicamente tras un callback. La primera regla de la estrategia es:

 

Regla 1. Envolver los controles ASP.NET que se van modificar tras un callback con etiquetas div, siguiendo la regla, <div id=div_AlgúnIdentificador>

 

El propósito de estos identificadores es usarlos más adelante como parámetros de una subrutina JavaScript generalizada.

 

Luego, observe que dentro de la etiqueta <head> incluí el archivo JScript CallbackComplement.js (yace en la ruta App_Scripts del sitio Web o donde guste). Este código JavaScript es el único que usa mi estrategia para implementar Ajax con callbacks, y es universal a todos los casos. Es conveniente colocar código JavaScript en archivos JScript, ya que el IDE de Visual Studio 2005 permite colocar puntos de interrupción, y verificar valores de variables, lo que no ocurre cuando colocamos directamente el código JavaScript en la página. La inclusión de el archivo CallbackComplement.js es la segunda regla:

 

Regla 2. Incluir el archivo de CallbackComplement.js en la página.

 

Código de CallbackComplement.js

/* Callback client code

 * By Harvey Triana

 * callbackResult is a compose string

 * p[0] is the id of tag <div Id=p[0]> ... </div>

 * p[1] is html stream

 */

function putCallbackResult( callbackResult ) 

{    

    if(!(callbackResult == ""))

    {

        var p = callbackResult.split('|');

        var div = document.getElementById(p[0]);

        div.innerHTML = p[1]; 

    }

}

function clientErrorCallback( error, context ) 

{    

    alert('Callback failed! ' + error); 

}

 

La función putCallbackResult tiene la tarea de escribir dinámicamente la repuesta de un callback en la página.

 

El Código del Servidor

 

Para capacitar Ajax basado en callbacks debemos implementar la interfaz abstracta “ICallbackEventHandler”, configurar las llamadas asíncronas, y elaborar una respuesta. Dispongo el código, y explico los detalles a continuación.

 

Código C# de la página CBStrategy.aspx

/*

 * Ejemplo del articulo: Síntesis Generalizada de Ajax con Callbacks

 * Por Harvey Triana

 */

using System;

using System.Data;

using System.Configuration;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.HtmlControls;

using System.IO;

using System.Text;

using System.Data.OleDb;

 

public partial class CBStrategy : Page, ICallbackEventHandler

{

    string m_CallbackResult;

 

    protected void Page_Load(object sender, EventArgs e)

    {

        // configurando los llamados a RaiseCallbackEvent y GetCallbackResult

        ClientScriptManager cs = Page.ClientScript;

        string rb, er;

 

        er = ClientScript.GetCallbackEventReference(this,

                                                    "arg",

                                                    "putCallbackResult",

                                                    "null",

                                                    "clientErrorCallback", true);

 

        // función que llamará a RaiseCallbackEvent

        rb = "function callServerTask(arg) {" + er + ";}";

 

        // inserta el script en la página

        cs.RegisterClientScriptBlock(this.GetType(), "callServerTask", rb, true);

 

        //  inserta el evento de llamada asíncrona

        ddlPaíses.Attributes.Add("onchange",

                                 callServerTaskForm(

                                 "IdPaís",

                                 this.ddlPaíses.ClientID));

 

        //  usar los datos tras un bóton Enviar

        btnEnviar.Attributes.Add("onclick",

                                 callServerTaskForm(

                                 "Submit",

                                 this.ddlPaíses.ClientID,

                                 this.ddlDivisiones.ClientID));

 

        // iniciar datos en las listas

        if (!Page.IsPostBack)

        {

            if (DataService.PopulateDataList(MapPath("~/App_Data/Sample.mdb"),

                                             ddlPaíses,

                                            "Países",

                                            "NombreDePaís",

                                            "IdPaís", "", "") > 0)

            {

                // poblar divisiones por primera vez

                DataService.PopulateDataList(MapPath("~/App_Data/Sample.mdb"),

                                             ddlDivisiones,

                                            "DivisionesPaís",

                                            "NombreDeDivisiónPaís",

                                            "IdDivisiónPaís",

                                            "IdPaís", ddlPaíses.SelectedValue);

            }

        }

    }

    public void RaiseCallbackEvent(string eventArgument)

    {

        StringWriter sr = new StringWriter();

        HtmlTextWriter htm = new HtmlTextWriter(sr);

 

        string[] p = eventArgument.Split(',');

        m_CallbackResult = "";

 

        switch (p[0])

        {

            case "IdPaís":

                DataService.PopulateDataList(MapPath("~/App_Data/Sample.mdb"),

                                             ddlDivisiones,

                                            "DivisionesPaís",

                                            "NombreDeDivisiónPaís",

                                            "IdDivisiónPaís",

                                            "IdPaís", p[1]);

                // respuesta html

                this.ddlDivisiones.RenderControl(htm);

                htm.Flush();

                m_CallbackResult = "div_ddlDivisiones|" + sr.ToString();

                break;

 

            case "Submit":

                this.lblInfo.Text = string.Format(

                                    "IdPaís = {0}, IdDivisiónPaís = {1}",

                                    p[1], p[2]);

                this.lblInfo.RenderControl(htm);

                htm.Flush();

                m_CallbackResult = "div_lblInfo|" + sr.ToString();

                break;

        }

    }

    public string GetCallbackResult()

    {

        // m_CallbackResult es pasada como argumento del js putCallbackResult

        return m_CallbackResult;

    }

 

    string callServerTaskForm(string Identifier, params string[] a)

    {

        // retorna el llamado a callServerTask y su parámetro

 

        string s = string.Format("callServerTask('{0}' + ", Identifier);

 

        for (int i = 0; i < a.Length; i++)

        {

            s += " ',' + getElementById('" + a[i] + "').value +";

        }

        // remueve el último '+' y termina el script

        s = s.Substring(0, s.Length - 1) + ");return false;";

        return s;

    }

}

 

Luego de implementar la interfaz ICallbackEventHandler, aceptamos el deber de escribir los eventos RaiseCallbackEvent y GetCallbackResult.

 

Observe que se declaró la variable m_CallbackResult. Nos es útil en todas las soluciones Ajax con callbacks. Contendrá la respuesta que elaboramos en RaiseCallbackEvent y que pasamos al la subrutina JavaScript  desde GetCallbackResult. No necesitamos modificar el procedimiento GetCallbackResult, pues será el mismo en todas las soluciones.

 

Luego configuramos las llamadas asíncronas con lo métodos GetCallbackEventReference y RegisterClientScriptBlock. Para esto podemos usar las variables cs, rb, y er en Page_Load, tal cual están definidas en el ejemplo, no es necesario hacer modificaciones en otra solución.

 

Regla 3. Definir la variable m_CallbackResult, programar los métodos GetCallbackEventReference y RegisterClientScriptBlock tal cual estan en el ejemplo, y copiar el procedimiento GetCallbackResult.

 

En seguida, debemos configurar los eventos de los controles ASP.NET, para general el callback. En primera instancia, configuro el evento onchange de la lista ddlPaíses, para que cuando esta cambie el ítem seleccionado, recargue la lista sometida, ddlDivisiones. Para esto se requiere agregar un atributo al control con un nombre de un evento, seguido de una llamada a una función JavaScript. Este evento solo sucede del lado del cliente. Técnicamente, inserta código html a la página. Explícitamente, el código del control del ejemplo en tiempo de ejecución queda de la siguiente manera:

 

Ejemplo de un evento del cliente para llamar un callback

<select name="ddlPaíses" id="Select1"

        onchange="callServerTask('IdPaís' +  ',' +

                  getElementById('ddlPaíses').value);return false;"

        style="width:300px;"> . . .

 

Atención, aquí hemos agregado un evento onchange del lado del cliente, que al disparase, llama a la subrutina callServerTask, con un solo argumento, el cual es una cadena de texto compuesta de otras cadenas, separadas por comas. Por ejemplo, cuando IdPaís  es igual a 1, esta llamada a la función callServerTask adquiere el siguiente texto en tiempo de ejecución: 

 

Invocación explicita del método JavaScript que llamará al callback

callServerTask('IdPaís, 1').value);return false;

 

Ahora el argumento de callServerTask es la cadena ‘IdPaís, 1’. Luego esta cadena es pasada al código del servidor, y recogida en RaiseCallbackEvent a través de su único parámetro, eventArgument. Esta cadena es analizada en el código del RaiseCallbackEvent, para elaborar una respuesta. Lo primero que se hace es analizar el primer elemento de la matriz de cadenas de texto, y según esta bandera aplicamos una directiva de selección de caso (switch en C#), decidimos que vamos a hacer. Sí ubica la línea “switch (p[0])” del ejemplo va a comprenderlo rápidamente.

 

En resumen, la cadena JavaScript para llamar a callServerTask contiene un parámetro, el cual es una cadena de texto compuesta de varias cadenas separadas por comas. El primer elemento es una bandera que evaluaremos en RaiseCallbackEvent, los demás son valores de controles, obtenidos por getElementById.

 

La elaboración del parámetro eventArgument en una línea de código es algo bizarro, pues usamos getElementById para cada control, y sumamos el carácter ‘,’ para separarlos. Así pues, escribí la función callServerTaskForm la cual simplifica la elaboracion de la cadena, y queda universal para todos los casos. Esta función solo requiere de una bandera, seguida de una lista de controles, identificados en la página por su ClientID.

 

Siguiendo con el ejemplo, analice lo simple que queda la configuración de un evento que llamará al callback:

 

Ejemplo de un evento del cliente para llamar un callback

        // inserta el evento de llamada asíncrona

        ddlPaíses.Attributes.Add("onchange",

                                 callServerTaskForm(

                                 "IdPaís",

                                 this.ddlPaíses.ClientID));

        // usar los datos tras un bóton Enviar

        btnEnviar.Attributes.Add("onclick",

                                 callServerTaskForm(

                                 "Submit",

                                 this.ddlPaíses.ClientID,

                                 this.ddlDivisiones.ClientID));

 

Regla 4. Configurar los eventos del cliente, facilitándose esto con la función callServerTaskForm la cual requiere de una bandera y una lista de controles identificados en la página por su ClientID.

 

Analice que el evento del botón “Enviar” queda en onclick, y que los valores actuales de las dos listas son pasados a callServerTaskForm. Es así como correctamente recolectamos los valores actuales de un formulario que usa callbacks. La bandera Sumit será procesada en RaiseCallbackEvent.

 

Por ultimo, voy a explicar como elaboramos la respuesta del callback desde RaiseCallbackEvent, la cual se compone de un identificador de bloque “div” y una fuente de texto html.

 

Si ha programado callbacks sabrá que para generar una fuente html necesitamos StringWriter, HtmlTextWriter, y aplicar el métdo RenderControl.

 

Regla 5. Según el caso, componemos m_CallbackResult con el identificador del bloque div donde vamos a reemplazar el html, seguido por un carácter “|”, más la fuente html construida con RenderControl.

 

Sí lee con atención la función JavaScript, putCallbackResult comprenderá la regla número 5 con facilidad.

 

Poblando las Listas

 

Como mencioné, los datos de las listas “Países” y “Divisiones Políticas”, pueden proceder de cualquier recurso. Para el ejemplo diseñe una simple base de datos de MS Access y coloque unos cuantos datos. Luego escribí  la función PopulateDataList en la clase DataService. La funcion PopulateDataList es útil para poblar cualquier DataListView desde una base de datos de MS Access.

 

Ejemplos en Código

 

El proyecto que escribí para exponer la estrategia lo puede descargar de la referencia 2. El proyecto incluye el ejemplo explicado en este articulo, más una extensión del mismo, CBStrategy2, que demuestra un ciclo completo de un formulario Web con escritura en la base de datos. Use una base de datos de MS Access, pero como sabrá, no es aconsejable en una producción real, en la que usaríamos un motor cliente-servidor, ya que todas las aplicaciones Web son cliente - servidor. La base de datos Sample.mdb es solo una ilustración.

 

Como ejercicio, en la clase que maneja los datos, Dataservice, usé código ASP.NET en su forma más especializada, es decir código no generado por asistentes.

 

 

Conclusiones

 

Es posible hacer una síntesis del la programación Ajax con Callbacks, de manera que podamos poner en producción rápida el asunto, enfocándonos en la parte puntual del negocio sin exagerar las tribulaciones del JavaScript implicado y en la implementación de la interfaz ICallbackEventHandler.

 

 

Referencias

 

1. Implementing Client Callbacks Without Postback in ASP.NET Web Pages

http://msdn2.microsoft.com/en-us/library/ms178208.aspx

 

2. Ejemplo (CBS.rar)

 

-----

 

Derechos Reservados. Este artículo no debe ser publicado en ningún medio publicitario o sitio de Internet sin previa autorización del autor.