Desarrollo Web con componentes: Intro a Wicket
Qué necesita resolver todo framework de presentación
En "web tradicional" las abstracciones están orientadas más al protocolo que usamos y no a los componentes que manejamos. ||Wicket busca hacer menos tedioso y más transparente la parte de presentación, combinando las ideas que vimos de las unidades anteriores:
- por un lado que la tecnología siga siendo web
- pero que podamos construir componentes visuales para facilitar la interacción con el usuario
Intro a Wicket
El proyecto que vamos a generar se puede descargar de los ejemplos: es el contador-ui-wicket, para ver el avance de cada iteración hay un tag específico.
Creamos un proyecto wicket desde cero siguiendo el instructivo de la cátedra.
¿Qué cosas intersantes aparecen?
Nuevas abstracciones
- Tenemos un objeto Application que extiende de WebApplication
- funciona como un singleton
- todo lo que sea transversal a la aplicación va a estar en este objeto
- al definir una aplicación tenemos que implementar el método getHomePage, que devuelve nuestra página de inicio
- También hay objetos WebPage en lugar de las JSP
- mientras que las JSP son stateless, las web page son stateful (y eso es algo beautiful). Cada variable de una Web Page funciona como las variables de instancia de los objetos que siempre conocimos:
class CalculadoraPage extends WebPage { Calculadora calculadora
Esto funciona como uno espera: cada vez que un usuario accede a la página de la calculadora, esa página tiene estado: un objeto Calculadora. Claro, eso se mapea como un session.getAttribute("calculadora"). Pero eso se mantiene transparente para nosotros.
- La WebPage tiene un constructor por default, que permite pasarle parámetros
new(final PageParameters parameters) { ...
Dentro del constructor definimos qué componentes va a tener la vista (sí, componentes que se programan en Java, donde valen todas las herramientas de Objetos nuevamente). Esto se parece más a lo que hemos visto en la Unidad 2, sólo que con los template methods que teníamos la construcción de la vista se reducía a redefinir ciertos métodos.
Una WebPage extiende de Page (que a su vez extiende de Component). La page define un set de components, como los Label, TextField (se forma así un Composite Pattern).
<<TODO: Hacer un diagrama.>>
Vista y controller
Wicket es un Framework Orientado a componentes que (como todos) dice ser MVC.
Cada web page tiene asociado un html
- el HTML resuelve los temas de vista: layout y widgets
- El html tiene un tag indentificado por un atributo especial wicket-id, eso configura un "hueco" que se va a completar con información dinámica.
- No hay lenguaje de templates ni scriptlets, la única intrusión al html es ese id, se supone que eso permite que lo escriba directamente un diseñador. Se puede ver que el html funciona aún sin el wicket, eso es algo saludable para tener un preview.
- la parte java resuelve la lógica de la vista (funciona como un controller dentro de MVC)
- cada componente procesa su tag, tiene que haber una correspondencia 1:1 entre el control en el html y el componente en la page de Wicket:
- si agregan un componente en la clase y no lo muestran en el html (y están ejecutando en modo developer) les va a tirar un error
- si en el html agregan componentes que no están en la clase java va a tirar un error
- si referencian más de una vez el mismo id de componente va a tirar un error
- cada vez que presionamos F5 (o actualizar) se crea una nueva instancia de la página (se resetean las variables)
La configuración del web-inf.xml es en la mayoría de los casos redundante: ¿para qué forzarlo a definir si en el 95% de los casos no cambia? Por otra parte, si definimos un contrato y no permitimos hacerlo configurable eso nos impide poder cambiar el default. La solución es tener una convención que evite tener que definir un montón de archivos xml de cosas que dicen una y otra vez lo mismo, y usar el xml para cuando necesitemos cambiar la configuración default. ¿Qué convenciones tiene Wicket?
- Los nombres del par .html y .java: se asume por default que una página que se llama editarSocio.html tendrá su correspondiente editarSocioPage.java donde está la lógica de presentación.
- de la misma manera, cuando definimos un control con su wicket:id, significa que tiene que manejarse el binding con una propiedad que tenga el mismo nombre en el .java. En caso de no existir esa propiedad Wicket da error.
- recordemos que vale la definición de propiedad de la unidad 2: no es necesariamente un atributo de una clase Java, sí sigue la convención de un java bean que implica:
- un método get para obtener el valor
- un método set para asignar un valor a esa propiedad
- si la propiedad es de sólo lectura (por ejemplo para un label) solamente tendremos el get de la propiedad (esto puede ser un get específico para mostrar por ejemplo el nombre completo de una persona, concatenando nombre y apellido)
Ejemplo iterativo
Avanzamos sobre un ejemplo partiendo del "hola mundo" del archetype de maven:
Iteración 1 : ejemplo básico casi "hola mundo"
- Crear aplicación básica con el archetype de maven para wicket
- Vemos la estructura de la aplicación
- Los componentes se deben crear en el momento de construcción de la página (en el constructor o en algún método que se invoque desde ahí).
Iteración 2 : links y ciclo de vida de página 1.
- agregar otro label que muestre un contador sólo en el html
<html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" > ... <body> ... <span wicket:id="contador">contador</span> </body> </html>
- esto como dijimos antes... explota (y está bien que sea así)
Unexpected RuntimeException
WicketMessage: Unable to find component with id 'contador' in [Page class = com.uqbar_project.edu.progui.claseInicial_ui_wicket.HomePage, id = 0, version = 0]. This means that you declared wicket:id=contador in your markup, but that you either did not add the component to your page at all, or that the hierarchy does not match.
Leemos el mensaje... es claro y se entiende: definimos un wicket:id pero no agregamos el componente en el controller.
- lo definimos y también agregamos links para incrementar/decrementar el contador.
<html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" > <body> <span wicket:id="contador">contador</span> <br/> <a href="#" wicket:id="sumar">Sumar</a> <a href="#" wicket:id="restar">Restar</a> </body> </html>
ContadorPage.html
class ContadorPage extends WebPage { extension WicketExtensionFactoryMethods = new WicketExtensionFactoryMethods int contador new() { this.addChild(new Label("contador", new PropertyModel(this, "contador"))) this.addChild(new XLink<Object>("sumar") => [ onClick = [| contador = contador + 1 ] ]) this.addChild(new XLink<Object>("restar") => [ onClick = [ contador = contador - 1 ] ]) } }
ContadorPage.xtend
- al sumar o al restar se refresca toda la página (se resetea el scroll)
- el comportamiento del botón se define como una inner class anónima en Java
add(new Link<Object>("restar") { @Override public void onClick() { contador--; } });
- es una clase ad-hoc que implementa Link<Object>. Como la clase no tiene nombre sólo se puede instanciar en el contexto en el que se crea la página.
- Link<Object> es una clase abstracta, que obliga a definir el método onClick(). En general los componentes default de Wicket son abstractos y los que diseñaron el framework prefieren la herencia por sobre la composición. Esto fuerza a que cada botón, link, etc. se defina como una implementación de Link con su comportamiento onClick específico. Para aquellos que trabajen en Xtend la librería uqbar-wicket-xtend les permite trabajar con closures como vimos anteriormente... esto permite bajar la verborragia de nuestro código:
this.addChild(new XLink<Object>("restar") => [ onClick = [ contador = contador - 1 ] ])
- la estrategia de Arena, en cambio, consiste en pasarle un objeto y un mensaje a enviar.
- ¿Qué tiene que hacer el link?
- no puede ser más fácil, sólo actualizar el contador: contador++;
- ahí se nota la ventaja de tener estado, no hay que ir a buscar el contador a ningún session ni request ni nada raro.
- y también la ventaja de programar la lógica separado del "rendering".
- al abrir otro tab del browser, el contador arranca de nuevo en cero (cada sesión es aislada, se puede trabajar en multisesión)
- mientras que el primer label que muestra el mensaje es estático, el link que muestra el contador trabaja sobre un model...
¿Por qué es esto?
Si reemplazamos el Label por:
this.addChild(new Label("contador", "" + contador))
no se refresca el valor del contador. Si queremos que al actualizar la página se vea reflejado el cambio, tenemos que usar un model de wicket
Para aprender más sobre models de Wicket, pueden ver esta página.
Para aprender más sobre controllers de Wicket, pueden ver esta página.
Iteración 3: ajax
- modificar el botón por un AjaxLink
class ContadorPage extends WebPage { extension WicketExtensionFactoryMethods = new WicketExtensionFactoryMethods int contador new() { val labelContador = new Label("contador", new PropertyModel(this, "contador")) this.addChild(labelContador) this.addChild(new XLink<Object>("sumar") => [ onClick = [| contador = contador + 1 ] ]) this.addChild(new XAjaxLink<Object>("restar") => [ onClick = [ target | contador = contador - 1 target.add(labelContador) ] ]) } }
- cambia el método: del onClick() sin parámetros pasamos al onClick(target) donde el target es el componente que se va a actualizar
- ahora necesitamos guardarnos una referencia (al Label) y definir qué componentes hay que actualizar del lado del cliente
- en Wicket proliferan las inner classes, como vemos, pero el uso de un XAjaxLink nos permite simplemente definir el comportamiento del onClick
- podríamos refactorizar para reutilizar el código entre acciones (chiche), pero por ahora nos concentramos en hacerlo andar, porque... ¡explota!
Unexpected RuntimeExceptionRoot
cause: java.lang.IllegalArgumentException: cannot update component that does not have setOutputMarkupId property set to true. Component: [Component id = contador]
¿Cómo funcionaba Ajax?
- Enviamos un pedido al servidor en forma asincrónica.
- Cuando el servidor responde... wicket sabe que esa respuesta va a modificar algún componente de la página (no toda la página porque no tendría sentido usar Ajax entonces)
- ¿Qué componente/s se va/n a modificar? Eso se lo tenemos que decir nosotros, a través del poco intuitivo método setOutputMarkupId
new() { val labelContador = new Label("contador", new PropertyModel(this, "contador")) this.addChild(labelContador) labelContador.outputMarkupId = true ...
Entonces ahora sí Wicket sabe que el target va a sobreescribir el label (lo reconoce por el id de wicket).
- ¡mostrar la magia! Ahora sí funciona
- poner un system.out para que lo crean ! :)
- mostrar que no interfiere con otro tab / usuario
Observaciones:
- no necesitamos nada de javascript
- ajax está modelado con objetos
- wicket se encarga de refrescarlos, solo tenemos que decirle qué componentes.
- ¡la página sigue siempre viva! ¡la misma instancia! ¡misma variable contador!
Para hacer en casa / Iteración 4: utilizando MVC
- sacar el contador a un modelo Contador.
- ver que explota el label de "contador" porque busca la property en HomePage.
- mostrar que usa convention over configuration, el id es la property.
- ver que los labels son diferentes:
- uno tiene un valor fijo, hardcodeado
- el otro sale del modelo.
- ver que explota el label de "contador" porque busca la property en HomePage.
Observaciones:
- Como la página es un objeto vivo, también su modelo.
- se parece a la idea de MVC (y luego de MMVC) que vimos en arena.
- el modelo puede ser un objeto normal POJO (plain old java object)
- ahora duplicamos la lógica del target ajax :(
- en Contador.contador vs HomePage.contadorLabel se ve el paralelismo entre modelo-vista. Suena un poco a "duplicidad".
- Con observers se podría llegar a codificar un mecanismo genérico para actualizar componentes ajax según los eventos del modelo.
Iteración 5: Navegación y ciclo de vida de páginas 2
Paso 1: páginas nuevas sin estado compartido
- agregar una nueva página que sólo muestre un label (no tiene mucho sentido a nivel de negocio, pero es solo para simplificar).
- agregar un link que vaya a la página nueva con el class
- en la página nueva, agregar un link de "volver"
Observaciones:
- Vuelve a una instancia de la página, nueva.
- ¿Cómo comparto un modelo entre páginas ?
Paso 2: ir a página nueva, pasando estado
- que el link instancie la página y le pase el contador
- que la página muestre el contador.
- que el back vuelva a una instancia nueva... (problemas! ....
Paso 3: volver a la misma instancia de la página original
- que la nueva página conozca a la original
- que vuelva a la misma instancia.
Para aprender más sobre navegación de una página a otra, podés ver esta página.