Wicket - Clase 2
29/10/2013 - UNSAM
Repaso de la clase anterior: con el archetype debería verse bien el html + css, si no se ve bien es porque no lo hicieron desde la línea de comando (el Wizard que hace New > ... no incluye el repositorio donde está el archetype específico de xtend que creamos nosotros)
Nuevos ejemplos
Una calculadora que permite ingresar:
- dividendo
- divisor
- un botón de dividir
- un label que muestre el resultado
Cómo se haría en grails:
- una vista gsp con dos textboxes con su name, un botón de submit y un label asociado, todo dentro de un form, ¿por qué? Para enviar los datos al servidor.
- luego un controller que recibe los parámetros, los convierte, delega en el modelo y llama a la vista
¿Y en Arena?
- Bindeo la vista contra un objeto de dominio directamente, con controllers chicos.
Diseñamos el objeto de dominio:
Calculadora
dividendo
divisor
resultado
+ dividir()
Y luego diseñamos la ventana (Window):
- TextBox dividendo <==> contra dividendo del modelo
- TextBox divisor <==> contra divisor del modelo
- Button <==> contra método dividir() del modelo
- Label resultado <==> contra resultado (y un controller que adapta de string a número)
Controles de HTML
- el TextBox es un input,
- el botón es submit,
- el label es un span,
- metido dentro de un form.
El div también es un container pero sin el cometido del form que sirve para capturar los parámetros del request. En Arena la comunicación se da naturalmente entre objetos porque viven en el mismo ambiente, esto no ocurre en Web porque hay una descentralización: un cliente y un servidor. Hay una separación lógica (y en muchos casos física), no puedo estar llamando al server cada vez que hay un evento de usuario que dispara un cambio.
Ahora sí, cómo lo pensamos en Wicket
En fin, Wicket tiene un modelo de objetos más que interesante pero no logramos que sea transparente el form que es dependiente de la tecnología web. La solución es mucho más parecido a Arena:
Vemos el código:
1) CalculadoraDivision implements Serializable
tiene @Property dividendo, divisor y resultado como double
un método dividir que delega a validarDivisor que veremos después
2) WicketApplication en getHomePage() llama a CalculadoraDivisionPage
3) CalculadoraDivisionPage tiene
- En lugar de Window, tenemos un objeto Page
- luego un objeto Form
- dentro un TextField que representa el dividendo
- y otro TextField que representa el divisor
- y un Button para disparar la división: new XButton.onClick [ | calculadora.dividir ]
- y por último un Label para mostrar el resultado.
- luego un objeto Form
¿Cómo ocurre la magia? Una vez que se submitea el form solito se "popula" o se actualiza el valor en el dominio antes de enviarle cualquier mensaje. Y luego del submit se vuelve a mostrar la misma página actualizando los nuevos valores que el dominio tiene (se los vuelve a pedir al recargar la página, no se crea una nueva sino que se refresca toda, salvo que hagamos el pedido vía Ajax).
¿Dónde le dijimos esto a Wicket? en la línea
class CalculadoraDivisionPage extends WebPage { new() { defaultModel = new CompoundPropertyModel(new CalculadoraDivision)
Esto genera esta asociación:
IModel, es una indirección
|
TextField --------o--------- Calculadora
-divisor
El IModel define un mensaje
- get --> devuelve un objeto (obtiene el valor de la propiedad), y
- y otro set(Object) que setea el valor de la propiedad del objeto de dominio.
El TextField tiene un modelObject que es un Object, pero en realidad es un atajo: en realidad define
- getModel() -> que devuelve un IModel. Y el IModel conoce al objeto de dominio.
La clase pasada vimos otro modelo: PropertyModel en el contador.
class HomePage extends WebPage { val label = new Label("contador", new PropertyModel(this, "contador"))
Si creamos una asociación contra un objeto nuestro, cuando se refresca la página no se ve modificado el valor de la propiedad. Esto es un poco confuso porque en Arena nuestro modelo era el objeto de dominio. En Wicket tenemos una interfaz IModel (get + set) como una indirección, como un Adapter o un Binder, porque el modelo está atrás.
De hecho IModel está en una jerarquía de Wicket que es un framework de UI, entonces es controversial hablar de modelo cuando lo resuelve la vista. Esto también pasa en SWT como TableModel, ViewModel, etc.
Volviendo a la calculadora, también podríamos usar el PropertyModel:
form.addChild(new TextField<Double>("dividendo"), new PropertyModel(calculadora, "dividendo"))
El primer dividendo asociado al TextField puede modificarse, en el CompoundPropertyModel hay una convención: se llaman igual la propiedad del objeto de dominio, el wicket-id del html y el nombre del control que definimos.
Error de modelObject: Si no cacheamos el valor del modelo en una variable de instancia, conviene pedir al form el modelObject:
val button = new XButton("dividir") button.onClick = [ | form.modelObject.dividir ]
Validaciones
¿Dónde queríamos que estén? En el dominio, en particular tenemos estas opciones
1) cuando dividen
2) en el setter
3) en el controller: transformers por ejemplo con los dates
Vemos las validaciones del dividir: tenemos un método validarDivisor, que tira una UserException:
def void validarDivisor(Double unDivisor) { if (unDivisor == 0) { throw new UserException("No se puede dividir por 0") } }
Las UserException las entendía Arena, Wicket no. Entonces agregamos un try/catch:
def addActions(Form<CalculadoraDivision> form) { val button = new XButton("dividir") button.onClick = [ | try { form.modelObject.dividir } catch (UserException e) { val error = new ValidationError() error.message = e.message button.error(error) }
Es burocrático el ValidationError porque tiene internacionalización. El código tiene 7 líneas de más y eso nos molesta, además cada botón que tire un error me exige agregar try/catch. Entonces ¿qué podemos hacer?
- una fácil es extender XButton, que cuando ejecuta el bloque hace un try/catch
- otra es recibir un Procedure0 (que hace un apply()) y armar el try/catch en XButton:
override onSubmit() { try { this.procedure.apply } catch (UserException e) { val error = new ValidationError error.message = e.message this.error(error) } } def setOnClick(Procedure0 procedure) { this.procedure = procedure this }
Creamos a continuación una clase xtend XForm que extiende de Form y redefinimos el constructor default, sobreescribiendo el process:
override process(IFormSubmitter subnittingComponent) { try { super.process(submittingComponent) } catch (WicketRuntimeException e) { if (e.cause instanceof UserException) { error(e.cause.message) } else { throw e } } } def setOnClick(Procedure0 procedure) { this.procedure = procedure this }
Reemplazando en CalculadoraDivision el Form por un XForm nos permite que en los setters salte el error de una manera más copada. Ahora, en el dividendo en -1 se muestra con error (porque está escrito adrede). Lo mismo con el divisor en 0. Ahora, ¿qué pasa si el dividendo es -1 y el divisor es 0? Si alguno de esos falla ya no se actualiza el modelo (da error y lo muestra pero pierde los valores que quería setear). Entonces vamos a crear un Validator:
el IValidator tiene un único método: validate()
def addFields(Form<CalculadoraDivision> form) { val dividendoTextField = new TextField<Double>("dividendo") dividendoTextField.add( [ validatable | ==> value es un IValidatable de un Double o puedo definirle un error if (validatable.value == -1) { val e = new ValidationError e.message = "No se puede -1" validatable.error(e) } ] def setOnClick(Procedure0 procedure) { this.procedure = procedure this }
Esto tiene un problema: corrimos la validación a la vista que era lo que no queríamos.
Soluciones de diseño:
- Pedirle al model que valide
Creamos un método validarDividendo y lo invocamos desde
dividendoTextField.add( [ validatable | form.modelObject.validarDividendo(validatable.value) ])
En el dominio definimos la validación y en la vista definimos cuándo se va a llamar.
- Una solución más práctica es tener un PropertyValidator. En lugar de darle un bloque, le decimos
dividendoTextField.add(new PropertyValidator)
El TextField tiene su propiedad y mediante la convención "propiedad" derivar en "validarPropiedad" con la mayúscula en la primera letra de dicha propiedad.
Ultimo ejemplo para ir ganando tiempo para la próxima clase
Búsqueda de celulares en Wicket
- el editar y el nuevo son la misma página pero reciben una instancia nueva o una existente.
- el appModel tiene un celularSeleccionado
- BusquedaCelularesPage usa un CompoundPropertyModel
- ¿Cómo funciona la navegación?
el onClick del nuevo llama a editar con new Celular(), mientras que la edición recibe cada uno de los celulares de la grilla e invoca a la página de edición:
def editar(Celular celular) { responsePage = new EditarCelularPage(celular, this) }
Me paso una referencia a mí mismo (this) para poder volver a la misma página original.
¡¡¡Podemos pasarle parámetros a la página!!!
Esto es idéntico a la navegación con Arena, porque las pages son objetos Java.
Además recuerdo la información de la página al volver...
def volver() { mainPage.buscarCelulares responsePage = mainPage }
¿Cómo funcionan las grillas?
Son ListView del lado de Wicket, que nos permite mostrar una colección de objetos como una lista, como paneles, etc. No es excluyente de las tables, tr y td de HTML.
listView.populateItem = [ item | item.model = item.modelObject.asCompoundObject y para llamar al editar ==> utilizamos item.modelObject ]
Vemos el html de eso: el id del componente XListView "resultados" se vincula contra el tr, no contra el table, porque el ListView hereda de AbstractRepeater y necesita trabajar con elementos repetitivos. Cambiamos el table, tr y td por ul y li sin modificar los .xtend y la visualización es diferente.
Otro tema de la navegación, por cada hijo se puede invocar a eliminar o editar porque tenemos el ítem "en la mano":
this.editar(item.modelObject)
Eso es bueno, no me obliga a trabajar con ids.
new XButton("eliminar").onClick = [ | buscador.celularSeleccionado = item.modelObject buscador.eliminarCelularSeleccionado ]
o bien podríamos hacer
... buscador.eliminar(item.modelObject) ...
Por default todas las acciones se quedan en la misma página. Es decir que implícitamente está sobreentendido que se hace:
new XButton("eliminar").onClick = [ | buscador.celularSeleccionado = item.modelObject buscador.eliminarCelularSeleccionado responsePage = this ]
pero no lo tengo que escribir. Sí puedo cambiar la responsePage si quiero navegar a otra URL, perdón, página.
También podría definir una página de error default:
new XButton("eliminar").onClick = [ | try { ... } catch (DBProgramException e) { responsePage = errorPage } ]
Al igual que la internacionalización es un tema cross aplicación. Wicket tiene una forma de setear una página de error general.