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
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?
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.>>
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)
Avanzamos sobre un ejemplo partiendo del "hola mundo" del archetype de maven:
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í).
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.
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!
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.
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.
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.