Friday, May 09, 2008

[AJAX] Carga dinámica de páginas ASPX con AJAX Enabled WebService

por Ing. Juan Pablo Ibañez
http://ing.juanpablo.googlepages.com

Con la aparición de AJAX, ahora podemos cargar ciertas partes de una página ASPX sin tener que recargar toda la página. El problema surge cuando en una página queremos incluir diferentes funcionalidades que habitualmente están en páginas separadas. Si intentamos incluir todas las funcionalidades en nuestra página e ir cargándolas mediante AJAX, nuestra página comienza a hacerse demasiado grande para poder albergar todo el código que permite cambiar las partes y mostrar las distintas funcionalidades sin recargar toda la página. Hay aplicaciones grandes que requieren utilizar otro enfoque, como ser cargar páginas ASPX dentro de páginas ASPX pero sin hacer PostBack de la página contenedora. La técnica explicada en este artículo nos permite tener nuestra aplicación dividida en todas las páginas ASPX que necesitemos, y cargarlas dentro de una página contenedora usando AJAX, lo cual nos permite dividir mejor la funcionalidad, distintos equipos pueden trabajar independientemente en cada página ASPX, nuestra aplicaciones está mas desacoplada, prolija y demás beneficios.

En el siguiente ejemplo voy a mostrar como cargar diferentes ASPX dentro de otra página ASPX haciendo uso de AJAX WebServices. La imagen [Imagen1] que vemos a continuación es un esquema de lo que se pretende hacer.


[Imagen1]

La página contenedora tiene 1 div donde se insertará mediante AJAX el HTML de las páginas ASPX que se quieran cargar dinámicamente dentro de la página contenedora. Si nuestras ASPX que queremos cargar tiene elementos que hacen PostBack, debemos cargarlas dentro de un iframe, de lo contrario, por ejemplo, al presionar un Button en la página que se cargo dentro de la contenedora, se hará un PostBack que recargará también nuestra contenedora y la idea es que la contenedora nunca se recargue completamente.

Nuestra solución entonces deberá tener al menos los componentes que se muestran en la siguiente imagen [Imagen2].

Como vemos en esta imagen del Explorador de Soluciones, tenemos:
          1. Default.aspx, nuestra página contenedora.
          2. Default.js, código Javascript para hacer la llamada al WebService y cargar el contenido que este devuelve en nuestro div.
          3. Page1.aspx, una de las páginas que queremos cargar dentro de Default.aspx
          4. Page2.aspx, otra de las páginas que queremos cargar dentro de Default.aspx
          5. PageLoader.asmx, el WebService que devuelve el HTML que vamos a cargar dentro del DIV de Default.aspx
          6. web.config, configuración para hacer uso de las librerías de AJAX ASP.NET y configuración para usar AJAX ASP.NET WebServices.
          7. ajax-loader.gif, imágenes que mostramos mientras se cargan las páginas interiores.




[Image2]

La idea es que nuestra página Default.aspx tenga un DIV donde cargar el HTML de las páginas que queremos cargar, haga una llamada a un AJAX Enabled WebService y mientras se carga la página interior muestre un Loading...

La estructura de nuestra página Default.aspx sería algo así:

<body>

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

        <asp:ScriptManager ID="ScriptManager1" runat="server">

            <Scripts>

                <asp:ScriptReference Path="Default.js" />

            </Scripts>

            <Services>

                <asp:ServiceReference Path="PageLoader.asmx" />

            </Services>

        </asp:ScriptManager>

        <div align="center">

            <h2>Página Contenedora</h2>

            <input id="btnLoadPage1" type="button" value="Cargar Página 1" onclick="return btnLoadPage1_onclick()" />

            <input id="btnLoadPage2" type="button" value="Cargar Página 2" onclick="return btnLoadPage2_onclick()" />

            <br />

            <hr />

            <br />

            <div id="Loading" style="display: none">

                <table style="width: 200px; height: 200px">

                    <tr>

                        <td align="center" valign="middle">

                            <img id="Img1" src="Images/ajax-loader.gif" alt="loading..." />

                        </td>

                    </tr>

                </table>

            </div>

            <div id="Target">

            </div>

        </div>

    </form>

</body>


Tenemos un ScripManager configurado para tener acceso a nuestro código JavaScript:

<Scripts>

  <asp:ScriptReference Path="Default.js" />

</Scripts>

y también le configuramos el path a nuestro WebService para que pueda crear automáticamente el proxy que vamos a usar desde nuestro código JavaScript:
<Services>

    <asp:ServiceReference Path="PageLoader.asmx" />

</Services>


También tenemos 2 HTML Buttons, uno para llamar a Page1.aspx y otro para Page2.aspx y además 2 divs, uno para mostrar la imagen de Loading mientras se carga la página interior y otro para cargar el HTML que devuelve el WebService.


Al presionar cualquiera de los botones se hace una llamada al AJAX Enabled WebService como vemos en el siguiente código:


function btnLoadPage1_onclick()
{
    ret = PageLoader.LoadPage("Page1.aspx", OnComplete, OnTimeOut, OnError);
    document.getElementById('Loading').style.display = 'block';
    return(true);
}
function btnLoadPage2_onclick()
{
    ret = PageLoader.LoadPage("Page2.aspx", OnComplete, OnTimeOut, OnError);
    document.getElementById('Loading').style.display = 'block';
    return(true);
}

function OnComplete(args)
{
    document.getElementById('Target').innerHTML = args;
}

function OnTimeOut(args)
{
    alert("Service call timed out.");
}

function OnError(args)
{
    alert("Error calling service method.");
}


PageLoader es el proxy creado por el ScriptManager y LoadPage es el [WebMethod] expuesto por nuestro WebService. La llamada es asíncrona por lo que nos pide que especifiquemos la función que se va a llamar cuando se haya completado la petición, la función que se llama si se produce un time out y la función que se llama si se produce un error.

O sea que las funciones de los botones hacen 2 cosas: la petición y mostrar la imagen de Loading.

Una vez que el AJAX Enabled WebService devuelve el HTML, cargamos este dentro del div "Target" en la función OnComplete.

Lo que queda ahora es ocultar el Loading, esto lo hacemos desde la página interior, por ejemplo, Page1.aspx, en el evento onload en el body:

<body onload="parent.document.getElementById('Loading').style.display='none';" style="margin:0px;">

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

        <table style="width:200px;height:200px;">

            <tr>

                <td valign="middle" align="center" style="background-color:Blue;color:White">

                    Page1.aspx

                </td>

            </tr>

        </table>

    </form>

</body>


Solo quedaría ver el WebService, que lo que hace basicamente es devolver un HTML con un IFRAME con la página que le pasamos como parámetro, de esta forma si nuestra Page1.aspx o Page2.aspx hacen un PostBack, este no afecta a la página contenedora.


[WebService(Namespace = "http://tempuri.org/")]

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

[System.Web.Script.Services.ScriptService]

public class PageLoader : WebService

{

    [WebMethod]

    public string LoadPage(string pageName)

    {

        return @"<iframe frameborder='0'

                         scrolling='no'

                         marginheight='0'

                         marginwidth='0'

                         height='200px'

                         width='200px' id='frame' src='" + pageName + "' runat='server'></iframe>";

    }

}


Es importante destacar la siguiente línea que es la que permite que el hacer AJAX Enabled al WebService:


[System.Web.Script.Services.ScriptService]


El siguiente gif animado muestra el ejemplo funcionando:






13 comments:

Carlos Zanini said...

Hola Juan Pablo, el problema de esto es que IFRAME es propio de IE.
De todas formas para intranets donde la plataforma es SOLO microsoft por lo que el IE es el unico browser esto mismo hubiera sido posible sin ajax y con un IFRAME fijo al que desde javascript se le asignaría la página que debe mostrar (algo como objetoIframe.src = 'Pagina2.aspx';)

En el caso de que el WebService NO devuelva un html del IFRAME sino el HTML resultado de un ASPX entonces el problema será la dificultad de hacer postback sobre el ASPX dinamico.. :P

Saludos

Carlos Zanini

Ing.JuanPablo said...

Carlos, lo he probado tanto en IE7 como en Firefox y anda a la perfección. Uso el IFRAME para no tener que renegar con el PostBack, si no usara el IFRAME todos los PostBack de la página interna. Pero de todas formas con un poco mas de JavaScript y AJAX Enabled WebServices podría lograr los PostBack de la página interna.

Ing.JuanPablo said...

Carlos, continuo con mi respuesta. Si bien tenés razón en decir que se puede tener un IFRAME estático y cambiar la página ASPX mediante JavaScript, tendrías que harcodear los nombres de las páginas ASPX en código JavaScript. Si tu lógica requiere mas complejidad a la hora de decidir que página ASPX cargar en el IFRAME tendrías que ir al servidor a procesar la lógica de negocios y retornar la página correcta, lo cual hace el artículo válido para estos casos.

Carlos Zanini said...

Me hiciste buscar info y veo que el IFRAME es utilizado en las ultimas versiones de casi todos los browsers por lo que el margen de usuarios que se queda afuera del IFRAME es muy poco (un 5%).

Por los nombres de las páginas, ya están hardcodeados en las funciones "btnLoadPage1_onclick" y "btnLoadPage2_onclick" en el ejemplo que diste, pero entiendo que tambien podrías enviar cualquier string al server y mediante lógica de negocios devolver una página. No te estaba criticando el post, el cual esta bueno, e incluso esta forma de comunicarse con el server es más rápida que si hubieses usado UpdatePanel.

Volviendo al IFRAME, realmente nunca me gustó. Al principio parece fácil de programar una aplicación aprovechando esa funcionalidad pero siempre surgen cosas que te exigen escribir más código del normal que hubiera requerido como por ejemplo cuando la página contenida dentro del IFRAME pierda la session o necesite un usuario autenticado y esto normalmente lleva a una página de login 'especial' que se muestre correctamente en el tamaño del IFRAME.

Es como dijiste vos que usando un poco más de Javascript y AJAX enabled webservices podrías hacer postback a la pagina interna (si no usaras IFRAME), pero si la aplicación llega a ser grande con tanto codigo todo junto el dia de mañana al que le toque modificar eso te va a odiar de por vida.

Saludos!

Ing.JuanPablo said...

Carlos, antes de seguir intercambiando ideas, te quiero agradecer las críticas constructivas que me estás haciendo, y también agradecerte por haber leído el post y tomarte la molestia de comentarlo.

Me doy cuenta que el ejemplo que hice no fue el mejor, pero la idea principal era mostrar mas que todo que era posible hacerlo haciendo uso de un AJAX Enabled WebService, que como bien ambos coincidimos, es mucho mas rápido que usar un UpdatePanel.

Coincido con vos que el IFRAME no es lo mejor en todas las situaciones y hay que identificar bien en que casos usarlo y en cuales no. También el tema del tamaño del IFRAME es un problema.

Para el ejemplo ni siquiera me hubiera hecho falta usarlo.

A la técnica le veo mucha utilidad para no tener todo junto en una página ASPX que oculte y muestre cosas para las distintas funcionalidades.

Saludos y gracias de nuevo.

Unknown said...

Hola Juan Pablo..
Tu post me parece muy interesante y a la vez practico para utilizar. PEro tengo un problema cuando creo la solucion tal como la planteas me genera error en el web service. Derrepent c me esta pasando algo no se si seria mucho molestia si pudieras enviarme el proyecto o pongas un link en tu articulo para descargarlo.. seria de gran ayuda.
De antemano Gracias..

mi mail: kanonsaga86@hotmail.com

NatiNog said...

Juan Pablo, gracias muy buen ejemplo, me sirvió muchísimo!
Saludos
Natalia

Victor Zapata said...

sabes que no me funciona no se que es lo que estoy haciendo mal pero lo tengo tal cual lo tienes tu, bueno no se que configuracion poner en el web.config ya que no diste nada para poner en ese archivo.

Ing.JuanPablo said...

Victor cuando creas applicaciones con AJAX ASP.NET el web.config tiene algunas entradas mas que el web.config tradicional. Lo que te recomiendo es que creas tu nuevo sitio a partir de la plantilla AJAX Enabled WebSite que te instala cuando instalas las extensionas de AJAX ASP.NET.
Saludos

lerry said...

Hola Juan pablo, muy bueno el tutorial, yo quiero hacer exactamente lo mismo pero utilizando php, como lo haria?? en que crees que cambiaria el codigo si intento hacerlo con php?? tus sugerencias serian muy importante para mi, gracias...

Wainer De Paula said...

¿Podrías enviar el archivo de proyecto o los archivos que componen el problema?

Adolfo Mejía Garay said...

Hola Juan Pablo es exelente tu ejemplo, lo he probado me funciona correctamente; pero me gustaría saber cómo redimencionar o darle el alto adecuado, ya q cada pagina a abro tiene una altura diferente y te agradeceré infinitamente q me des la idea de como calcular el alto de la página devuelta por el servidor para establecer el alto del IFRAME, para q no sea estático.

Gracias de antemano::
Atte: Adolfo M.

Juankmiloo said...

Hola Juan Pablo,

Realmente segui los pasos que tu indicas pero no me funciona, me sale un error en el javascript me dice que no encuentra al pageloader. No se si me puedas colaborar mas con el tema, lo que sucede es que tengo un menu realizado con jquery y necesito cargar paginas asp.net dentro de un div y la verdad no he podido hacerlo, ya que soy un poco amateur en el tema. Tu me podrias ayudar?