Вы находитесь на странице: 1из 12

AJAX en aplicaciones J2EE con DWR

1. AJAX

Básicamente, AJAX (Asynchronous JavaScript and XML) es una "filosofía de desarrollo" que pretende mejorar
la usabilidad de las aplicaciones web, evitando la recarga continua de diferentes páginas HTML. Cuando se
ejecuta cualquier lógica de negocio, en lugar de devolver una página completamente nueva (generada en el
servidor) sencillamente, se actualiza (en el cliente) el código HTML de la página actual.

Figura 1. Comparación de Ajax con la filosofía tradicional

La filosofía AJAX es posible gracias a dos tecnologías:

1. DHTML (Dynamic HTML).Que permite modificar, en el cliente, la estructura de la página web que se está
visualizando.
2. XMLHttpRequest. A través de este tipo de objetos, se puede, desde un navegador (desde un script JS)
abrir una conexión HTTP independiente con un servidor.

El proceso es sencillo: un evento de usuario (por ejemplo, "onclick" en un botón), dispara la ejecución de una
función JavaScript. Esta función, incluirá una llamada a un objeto XMLHttpRequest que establecerá una
conexión con el servidor, solicitando la ejecución algún mecanismo similar a un procedimiento remoto (puede
ser un servicio web, una Action de Struts o, simplemente, un Servlet). Finalmente, el resultado obtenido de
esta llamada se plasma en la actualización (a través del API DOM) de la página web (por ejemplo, un botón
podría ocultarse, podría aparecer un DIV completamente nuevo con información recibida del servidor, el
contenido de una tabla se podría actualizar, etc.).
2. Direct Web Remoting (DWR)

DWR (http://getahead.ltd.uk/dwr/) es un API que permite introducir, en las aplicaciones J2EE, la filsofía AJAX
de una forma sencilla. DWR permite realizar llamadas remotas desde código JavaScript (ejecutándose en un
navegador), a objetos Java (POJOs) del servidor.

DWR no es, o al menos no pretende ser, únicamente una librería para llamadas remotas desde JavaScript. En
este sentido, existen productos más potentes, por ejemplo, JSON­RPC. DWR es AJAX. Tan importante es la
realización de llamadas a objetos del servidor como la actualización "en caliente" de la página web que se
muestra al usuario.

DWR se compone de dos partes: una parte que se ejecuta en el lado cliente (en este caso, un navegador web) y
otra parte que se ejecuta en el servidor (en este caso, un contenedor de Servlets).

Figura 2. Esquema del funcionamiento de DWR

2.1. DWR en el Servidor

En el lado del servidor, DWR proporciona servicios para la invocación de métodos remotos y la traducción de
tipos, de forma muy similar a otras tecnologías RPC.

Invocación Remota (Directa) de Métodos
  DWR proporciona un envoltorio (wrapper) para permitir la invocación de métodos de objetos Java
(POJOs) desde el cliente. El sistema de RPC implementado por DWR se basa en un Servlet
(uk.ltd.getahead.dwr.DWRServlet), el cual, utilizando la información que le llega a través de la
petición HTTP (HTTP Request), se encarga de instanciar los objetos necesarios y de realizar la invocación
del método solicitado, pasándole los parámetros enviados desde el cliente. Todas estas operaciones se
realizan utilizando el API de reflexión de Java.

Traducción de Tipos
  DWR puede realizar, de forma automática, traducciones de varios tipos: tipos básicos, colecciones, arrays
y beans. DWR siempre intenta traducir tipos Java al tipo JavaScript más parecido. Por ejemplo, las
colecciones se pueden traducir a arrays y los beans a arrays asociativos, siendo los nombres de las
propiedades del bean los índices del array.
2.1.1. Configurar el servidor

El motor de DWR es el objeto DWRServlet. En esta clase se centralizan todas las posibles funcionalidades que
ofrece la librería DWR: desde la generación del código JavaScript a utilizar en el cliente, hasta el marshalling
de tipos (pasando, por supuesto, por la invocación a los métodos remotos).

Como cualquier otro servlet, DWRServlet debe ser declarado (y "mapeado" a alguna URL) en el fichero WEB-
INF/web.xml de la aplicación web.

<servlet>
<servlet-name>dwr-invoker</servlet-name>
<display-name>DWR Servlet</display-name>
<servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>

2.1.2. Exportar objetos Java

Cualquier clase puede exportar métodos utilizando DWR. Todas las declaraciones necesarias, tanto para
exportar métodos, como para realizar traducciones de tipos se realizan en el fichero: dwr.xml. En este fichero
se realizará toda la configuración necesaria, tanto para el lado servidor como para el cliente. 

Nota

Antes de exportar métodos de objetos es muy recomendable valorar las posibles
consecuencias (principalmente para la seguridad, pero también para la mantenibilidad del
código e incluso para la integridad de la propia lógica de negocio de la aplicación). Una
buena idea suele ser utilizar el patrón de diseño "FrontController": únicamente se
expondrán los métodos de determinados objetos, cuya única responsabilidad es únicamente
esa: exportar métodos que puedan ser llamados directamente por el cliente (navegador). A
partir de ahora, el concepto "Controlador DWR", se referirá siempre a este tipo de objetos.

El fichero dwr.xml tiene dos partes:
1. Declaración de mapeos de tipos. Se definen con la etiqueta <convert>. Cada una de las entradas, en el
fichero dwr.xml, etiquetada como <convert> define un mapeo de tipo. Para cada tipo "no estándar"
(siendo tipos estándar: int, boolean, float, String, Collections, etc.) se debe definir un mapeo.
2. Declaración de clases a exportar. Las clases (controladores DWR) cuyos métodos podrán ser llamados
desde el cliente, se definen con la etiqueta <create>. En esta etiqueta se indica cómo se deben crear y
manejar las instancias de objetos exportados.

<dwr>
<allow>
<convert converter="bean" match="es.princast.framework.core.vo.PropertyBean"/>
(1)
<create creator="session" javascript="MunicipiosController"
class="es.princast.sampleapp.web.dwr.MunicipiosController"> (2)
</create>
</allow>
</dwr>

(1) Para convertir un tipo Java­JavaScript se deben indicar dos parámetros (atributos de la etiqueta
<convert>): 

a. La clase Java a convertir.
b. El nombre del "convertidor" (Converter) que se va a encargar de realizar el marshalling. Existen
varios convertidores incluidos en DWR (el más habitual es el converter bean, que transforma beans a
arrays asociativos JavaScript). También es posible implementar convertidores propios.

Nota
Si la aplicación web está bien diseñada, entre las distintas capas de la aplicación,
únicamente se producirá intercambio de objetos de tipos simples o beans (Patrón Data
Transfer Object), por lo tanto, casi siempre se utilizará el "bean converter".

(2) Mediante la etiqueta <create> se definen los objetos que se van a exportar. Los atributos necesarios son:
a. El nombre completamente cualificado de la clase Java a exportar (atributo class).
b. El nombre identificativo en JavaScript de la clase (atributo javascript). Todos los métodos de la
clase se exportarán a JavaScript como funciones, con el convenio de nombrado: <Nombre
JavaScript>.<nombre del método>
c. El ámbito (scope) del objeto (atributo creator).

2.2. DWR en el cliente

La parte de DWR que se ejecuta en el navegador, tiene dos funciones: por un lado, sirve como stub para la
realización de llamadas a los objetos del servidor, y por otro, proporciona un conjunto de funciones que
facilitan la operación sobre el código DHTML de la página web.

Stub del lado cliente
  Para poder utilizar las facilidades de llamada a métodos remotos en el servidor, es necesario importar, en el
cliente, el motor DWR, escrito en el fichero engine.js. Este fichero está ubicado en el path: <servlet­
dwr­path>/engine.js.

Además, DWR proporciona para cada uno de los métodos exportados por el servidor, una función
JavaScript que actúa como stub del cliente. Los sutbs de cliente ocultan al desarrollador las complejidades
de la comunicación cliente­servidor (especialmente, el manejo del objeto XMLHttpRequest).
Las funciones JavaScript, que hacen referencia a los métodos de una misma clase, se agrupan en un mismo
fichero .js. Cada uno de estos fichero se puede obtener siempre de la URL: <servlet­dwr­
path>/interface/<nombre­clase>.js.

No es necesario escribir los ficheros stub (.js) el servlet DWRServlet se encarga de generarlos "al vuelo"
utilizando la configuración que obtiene del fichero dwr.xml.

Por ejemplo, si se exporta la clase:
public class Foo {

public String doFoo() {


return "foo";
}
}

En la URL: <servlet­dwr­path>/interface/foo.js se encuentra un fichero de script que contiene funciones
que permitirán acceder a todos los métodos de la clase Foo. En concreto, la función " function
Foo.doFoo()" (en el cliente), permite acceder al método doFoo() de la clase Foo (en el servidor).

Para realizar llamadas a métodos del servidor, basta con utilizar las funciones de las librerias "interface".
No es necesario invocar ninguna función del fichero engine.js.

Utilidades para actualizar dinámicamente la página HTML
  Además de facilitar la comunicación con el servidor, DWR incluye una biblioteca de funciones que
permiten manipular el código DHTML de la página para actualizar sus contenidos, de forma dinámica.
Algunas de las operaciones que se incluyen son: actualizar el contenido de un DIV, cargar un "combo",
actualizar determinados campos de texto, etc.

2.2.1. JavaScript Asíncrono

La filosofía AJAX supone que la actualización de la vista se realiza de forma asíncrona. Es decir, el usuario
puede estar centrado (leyendo, operando, escribiendo, etc.) en una parte de la página y, simulatáneamente,
JavaScript está actualizando otra parte (o la misma!!).

Ya que el trabajo sobre la vista (página HTML) se realiza de forma asíncrona (multihilo), se debe tener en
cuenta que:

• Puede haber conflictos al actualizar simultáneamente los componentes de la página. Por ejemplo, si se envía
un formulario al servidor y, antes de que este conteste, se realiza otra operación (un listado), se pueden
producir inconsistencias en el estado de la aplicación.
• Las llamadas a métodos remotos deben ser asíncronas. Es decir, no se puede llamar directamente a un
método y obtener un resultado.
...
data = FooRemoteClass.fooMethod(); //Llamada remota con DWR
alert("Datos recibidos: "+data);
...
El código anterior no funcionaría, ya que se trata de una llamada síncrona. Para que una llamada remota con
DWR funcione, es necesario pasarle, como parámetro, una función de callback que se encargue de procesar
la respuesta del servidor. Por ejemplo:
...
data = FooRemoteClass.fooMethod(processData);
...

function processData(data) {
alert("Datos recibidos: "+data);
}

Este código realiza, de forma asíncrona, las operaciones que se pretendían ejecutar, en el primer ejemplo, de
forma síncrona.

2.2.2. Actualización dinámica de la vista

Aparte de simplificar la invocación de métodos remotos (en el servidor) desde clientes ligeros, DWR
proporciona un conjunto de funciones que facilitarán la actualización de la página web activa (que está viendo
el usuario), de forma dinámica, utilizando JavaScript.

Estas funciones se agrupan en un fichero de scripting, que se puede obtener de la URL: <servlet­dwr>/util.js. Al
igual que ocurre con otros ficheros JS servidos por el Servlet DWR, el fichero util.js se genera de forma
dinámica al ser solicitado (no es necesario incluir este fichero en la aplicación web).

Las funciones de utilidad van a permitir la actualización del árbol DOM de la página web activa, pero
ocultando, en la medida de lo posible, las complejidades del modelo de objetos DOM. Algunas de las
operaciones más comunes van a permitir:
• Establecer un valor (o conjunto de valores) para un componente. Estas operaciones (getValue(),
getValues(), setValue() y setValues()), permitirán actualizar el "valor" de un componente de la
página. El significado del valor (o valores) vendrá determinado por el tipo de componente sobre el que se
apliquen. Este conjunto de operaciones es aplicable a: campos de texto, radio­buttons, divs, etc.
Prácticamente se puede utilizar con cualquier tipo de componente salvo tablas, imágenes y listas.
• Añadir / Eliminar filas de una tabla, con las funciones addRow() y removeAllRows().
• Añadir / Eliminar opciones de una lista, con las funciones addOptions() y removeAllOptions().
• etc.

El siguiente fragmento de código permite actualizar el contenido de un DIV con el resultado de la invocación a
un método remoto:

<script languaje="JavaScript">
...
FooRemoteClass.getContenidoDiv(data);
...

function loadDiv(data){
DWRUtil.setValue("divId", data);
}
</script>
<body>
...
<div id="divId"></div>

</body>

El código del servidor que devuelve el contenido para el DIV será:

public String getContenidoDIv() throws ServletException, IOException{


return ExecutionContext.get().forwardToString("/contenidoDiv.jsp");
}

3. DWR. Un caso de uso.

Para finalizar, se presentará un ejemplo sencillo en el que se explicará, de forma detallada, cómo introducir
DWR en una aplicación J2EE para solucionar, de forma elegante, una funcionalidad relativamente habitual.

En ocasiones, es necesario presentar, en un formulario, un par (o más) de listas de selección (combo­boxes)
enlazadas. Estas listas de selección, tienen una relación maestro­detalle que las asocia, de tal manera que el
contenido de la segunda depende del valor seleccionado en la primera de ellas. Por ejemplo, un formulario
podría tener un campo de selección para que el usuario elija una provincia, y otro campo, de igual tipo, que le
permita elegir una población. El segundo de los campos (el de poblaciones) se debería actualizar,
dinámicamente, en función del valor selecionado en el primero.

Para dar salida a este requerimiento de una forma elegante, utilizando DWR, basta con seguir los siguientes
pasos:
1. Diseñar la página donde se van a albergar los combo­boxes enlazados.

En este ejemplo, se trata de un formulario de inserción de datos para el envío de un pedido. Se necesita
introducir una provincia y una localidad (que pertenecerá a dicha provincia), por lo tanto, en la página
habrá dos componentes SELECT, tal y como se puede ver en la siguiente imagen:

El código HTML correspondiente a los dos campos de selección es el que sigue:
<form name="envioForm" method="post" action="ejemplo/dwr/action/envioAction">
(1)
...

<div>
<label for="provincia">Provincia:</label>
<select name="provincia" size="1" class="cajaTexto"
id="provincia"> (2)
<option value="AST">Asturias</option>
<option value="MAD">Madrid</option>
<option value="BCN">Barcelona</option>
<option value="VAL">Valencia</option>
...
</select>
</div>
<div>
<label for="municipio">Municipio:</label>
<select name="poblacion" size="1" class="cajaTexto"
id="municipio">(3)
</select>
</div>
...

</form>

(1) Por simplicidad, se muestra el código HTML estático correspondiente al ejemplo. Para la generación
de este código se puede utilizar cualquier motor de plantillas (JSP, Velocity, etc.).
(2) El maestro, en este primer paso, será una caja (combo­box) de selección estándar. Las opciones se han
añadido de forma estática, en una aplicación real se podría utilizar JSP sin problemas para su
generación. La unica característica especial es el atributo "id" (en el ejemplo "id=provincia") que será
utilizado más adelante en los scripts.
(3) El detalle, en la página inicial, estará vacío (sin opciones). Mas adelante, se imeplementará un script
Ajax que se encargará de cargar las opciones según el valor seleccionado en el maestro. Además, al
igual que en el maestro, se debe incluir un atributo "id" para su uso en los scripts (en este ejemplo
"id=poblacion").
2. Implementar un objeto de negocio que permita obtener la lista de provincias que pertenecen a un municipio
determinado:

public class MunicipiosController {

public Municipio[ ] getMunicipios(String idProvincia) {

return DAOFactory().getMunicipiosDAO().getMunicipios
(idProvincia);

En este ejemplo, se ha implementado una clase: MunicipiosController, cuyo objetivo es aislar la capa
de negocio de la capa web. Esta clase se limita a llamar a un objeto de acceso a datos (patrón DAO) que se
encargará de buscar, por ejemplo, los municipios en una base de datos.

El resultado es un array de beans de tipo Municipio, cuya definición es la que sigue:
public class Municipio {

...

public String getCodigo() {


return this.codigo;
}

public String getNombre() {


return this.nombre;
}

...

Como se puede ver, esta clase se trata de un bean con dos propiedades: código y nombre.
3. Definir la parte del servidor

El siguiente paso será configurar la parte del servidor de DWR. Para ello, es necesario trabajar con el
fichero dwr.xml, pero antes es necesario configurar el motor DWR y para ello, se debe declarar el servlet
DWR (DWRServlet) en el fichero web.xml (ver apartado Sección 2.1.2).

En este fichero se definirán, tanto los mapeos de tipos para el cliente, como los interfaces remotos que se
expondrán.
<!DOCTYPE dwr PUBLIC
"-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN"
"http://www.getahead.ltd.uk/dwr/dwr10.dtd">

<dwr>
<allow>
<convert
converter="bean"
match="mi.ejemplo.Municipio"/>(1)
<create
creator="session"
javascript="MunicipiosController"
class="mi.ejemplo.MunicipiosController">(2)
</create>
</allow>
</dwr>

(1) Utilizando la etiqueta <convert> se definen mapeos de tipos. En este ejemplo, únicamente es
necesario exportar el tipo Municipio. Para su mapeo, se utilizará el conversor (converter) "bean".
Existen conversores para otros tipos: colecciones, numéricos, etc.
(2)   Los interfaces expuestos al cliente, se definen con la etiqueta <create>. En este ejemplo, se exportan
únicamente objetos de la clase mi.ejemplo.MunicipiosController. Se creará un objeto por
sesión (tal como indica el atributo "creator = session"). Desde el código JavaScript, se podrá acceder a
esta clase utilizando el nombre: MunicipiosController (definido por el atributo "javascript").
Una vez configurada la parte del servidor, la propia librería DWR (el DWRServlet) se encargará de
generar el stub del cliente. Este stub se puede obtener en la URL:
http://localhost:8080/appEjemplo/dwr/interface/MunicipiosController.js
4. Añadir código JavaScript al cliente
Para terminar, es necesario añadir, a la página JSP que se ha diseñado con anterioridad, el código
JavaScript que le permita mantener actualizados los dos combo­boxes. La página del ejemplo quedará
como sigue:
<script type="text/javascript"
src="../../dwr/interface/MunicipiosController.js"></script>(1)
<script type="text/javascript" src="../../dwr/engine.js"></script>(2)
<script type="text/javascript" src="../../dwr/util.js"></script>(3)
<script type="text/javascript">(4)
<!--

function init(){ (5)


DWREngine.setErrorHandler(errors);
update();
}

function errors(message) {(6)


alert("Errores "+message);
}

function update() { (7)


var provincia = DWRUtil.getValue("provincia");
MunicipiosController.getMunicipios(createList, provincia);
}

function createList(data) { (8)


DWRUtil.removeAllOptions("municipio", data);
DWRUtil.addOptions("municipio", data, "codigo", "nombre");
}
-->
</script>

...

<form name="envioForm" method="post" action="ejemplo/dwr/action/envioAction">


...

<div>
<label for="provincia">Provincia:</label>
<select name="provincia" size="1" onchange="update();(9)
class="cajaTexto" id="provincia">
<option value="AST">Asturias</option>
<option value="MAD">Madrid</option>
<option value="BCN">Barcelona</option>
<option value="VAL">Valencia</option>
...
</select>
</div>
<div>
<label for="municipio">Municipio:</label>
<select name="poblacion" size="1" class="cajaTexto"
id="municipio">
</select>
<script type="text/javascript">(10)
<!--
init();
-->
</script>
</div>
...

</form>

(1) Para poder acceder a los métodos exportados por el servidor es necesario importar el interface del
controlador de la URL: <url­servlet­dwr>/dwr/interface/<controlador>.js. En este caso, como es
controlador DWR se llama MunicipiosController, la URL será: ../../
dwr/interface/MunicipiosController.js.
(2) Para que el cliente pueda realizar llamadas remotas es necesario importar el "motor" de DWR. Este
fichero js se encuentra siempre en la ubicación: <url­servlet­dwr>/dwr/engine.js.
(3) En este ejemplo, la librería de utilidades "util.js" se utilizará para actualizar, de forma dinámica, el
contenido del combo­box detalle. Esta librería implementa funciones auxiliares que permiten
modificar el contenido de la página web de forma relativamente sencilla y elegante.
(4) Toda la lógica de presentación que enriquece el interface está ubicada en una etiqueta <script>, de
esta forma, se facilita el mantenimiento del código JavaScript (que ya de por si es bastante
complicado).
(5) Se ha implementado una función (init()) para la precarga de valores iniciales en el componente
detalle (llamando a update()). En general, se puede utilizar esta función para cualquier operación de
inicialización. En este ejemplo, además se establece un controlador de errores específico, utilizando la
función DWREngine.setErrorsHandler().
(6) El controlador de errores es una función que se dispara en caso de que se produzca algún error en la
ejecución de métodos remotos. Es posible definir un controlador de errores global (como se ha hecho
en el ejemplo, con la función errors()), un controlador de errores por llamada o bien, dejar que se
utilice el controlador de errores por defecto.
(7) La función update() se llamará cada vez que cambie el valor seleccionado en el combo­box maestro. El
objetivo de esta función es, ni mas, ni menos, que actualizar el componente detalle, manteniendo
siempre actualizada la relación maestro­detalle. Este método se encargará de solicitar al servidor la
ejecución del método getMunicipios(), de la clase MunicipiosController, llamando al método
del mismo nombre implementado por el stub del cliente. Este método recibe como parámetros: a) la
función de callback (en este ejemplo: createList()) que se encargará de gestionar el resultado
cuando responda el servidor (recordar que las llamadas Ajax son asíncronas). b) Opcionalmente, el
nombre de un controlador de errores específico y c) los parámetros del método remoto.

En esta función también se ha utilizado una función auxiliar (del fichero util.js) para obtener el
valor seleccionado en el componente maestro (DWRUtil.getValue()).
(8) La función createList() es el callback del cliente. Este tipo de funciones son las encargadas de
recoger, de forma asíncrona, la respuesta del servidor a una invocación remota. Los callbacks reciben
como parámetro el valor devuelto por dicha invocación. En este ejemplo, el valor es una lista de beans
de la clase Municipio. DWR se encargará de realizar, automáticamente el marshalling de tipos. La
lista de municipios se carga en el combo­box detalle utilizando una función de utilidad
(DWRUtil.addOptions()) que permite añadir una lista de beans a un campo de tipo select. Esta
función requiere los siguientes parámetros: a) el identificador "id" del campo destino, b) la lista de
beans, c) el nombre de la propiedad del bean de la que se tomarán los valores (values) de cada opción
y d) el nombre de la propiedad de los beans de la que se tomarán las etiquetas (labels) .
(9) Al componente maestro se le debe añadir un manejador para el evento "onchange". En este caso, el
manejador es la función update().
(10) A continuación de los dos componentes, se ha incluido una llamada al método init() para precargar
el contenido del detalle con los municipios de la primera provincia del maestro.
5. Empaquetar, desplegar y probar.

Una vez implementado todo el código, solamente queda construir el entregable, subirlo al servidor y
probarlo. Es muy importante tener activado siempre el intérprete JavaScript en el navegador. Si no hay
JavaScript, no hay Ajax.

3.1. Otros posibles casos de uso

Otros posibles escenarios en los que se puede utilizar DWR (o cualquier otra tecnología Ajax) pueden ser:

• Listados maestro­detalle. Un componente típico en aplicaciones basadas en "clientes pesados" son los
listados Maestro­Detalle. En los clientes ligeros (web) este tipo de componentes se emula recargando
completamente la página, lo que implica la renderización tanto de los datos del detalle (que pueden haber
cambiado) como los datos del maestro (que permanecen fijos). Con la filosofía Ajax, es posible recargar
únicamente la parte de la página correspondiente al detalle.
• Menús. Habitualmente, los menús en aplicaciones web se suelen implementar con JavaScript. Muchas veces,
se utilizan componentes implementados ad­hoc. Ajax permite extender este tipo de menús, con la mejora de
que el contenido de los mismos, se puede obtener, de forma dinámica, del servidor, siempre sin necesidad de
recargar toda la página.
• Gestores de Ventanas (iFrames). Con Ajax es posible dividir una página en pequeños componentes (similares
a las ventanas de los programas cliente­servidor), siendo el contenido de cada uno de estos componentes
actualizable, de forma independiente al resto. Sin utilizar la tecnología Ajax, este tipo de componentes se
implementan, actualmente, con iFrames o Portlets.
• Otros componentes avanzados. La filosofía Ajax permite implementar, en la parte del cliente, componentes
avanzados muy similares a los disponibles en aplicaciones más pesadas. Algunos de estos componentes
pueden ser: árboles, tablas dinámicas, paneles con posibilidad de arrastrar componentes, barras de
herramientas apilables, etc.

Acerca del autor. Ángel Retamar trabaja como arquitecto J2EE en diversos proyectos para el Principado de
Asturias, entre ellos, el desarrollo del FW­PA.

Вам также может понравиться