Vamos a comenzar a trabajar definiendo nuestra primera aplicación.
Creamos una category que se llame PAIU-Seaside-PrimerosEjemplos. Luego construimos una clase WAContador que extienda/herede de WAComponent: parados sobre la definición de la clase escribimos:
WAComponent subclass: #WAContador
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'PAIU-Seaside-PrimerosEjemplos'
WA es el prefijo que tienen todas las clases Seaside (por Web Application). Si vamos a desarrollar objetos que se encarguen de la vista estaría bueno respetar esta convención.
Definimos el siguiente método:
renderContentOn: html
html text: 'hola mundo'
Listo, ya tenemos nuestro componente, pero ¿cómo usarlo? Cómo hacer que sea accesible al usuario. Aquí vemos cómo deployar aplicaciones.
Pasamos entonces a un ejemplo más completo/interesante que el simple "hola mundo". Vemos entonces que el objeto que recibimos como parámetro del renderOn es de tipo WARenderCanvas. Miramos un poquito esta clase, para ver qué podemos hacer. Cada uno de los métodos de esta clase, se los llama "brushes". Así es la metáfora de seaside. Con esto métodos, vamos a ir "construyendo" o declarando cómo va a ser el contenido del canvas (el contenedor donde se dibuja la página), y por ende, al final, el html a generar.
Implementamos entonces el ejemplo de un Contador.
renderContentOn: html
html text: html class name.
html break.
html anchor: 'Contador'
Vemos entonces que "text", "break" y "anchor" son estos famosos "brushes": generan los tags <span>, un <br> y un <a href> respectivamente.
Ejemplo:
renderContentOn: html
html text: html class name.
html break.
html anchor
callback: [];
with: 'Contador'
El caracter punto y coma ';' nos permite encadenar mensajes al mismo objeto. Así en este ejemplo arriba estamos enviando los mensajes "callback" y "with" al mismo objeto respuesta del mensaje "anchor".
Acá se ve una particularidad de la idea de componente en seaside. El anchor es un mensaje que se envía a WACanvas. Entonces, el modelo de componentes en seaside se da
¿Qué otras formas tenemos para modelar la vista?
Avanzamos con el ejemplo. Definimos un nuevo componente CIUContador con un atributo "count"
WAComponent subclass: #CIUContador
instanceVariableNames: 'count'
classVariableNames: ''
poolDictionaries: ''
category: 'PAIU-Seaside-PrimerosEjemplos'
Creamos los accesors getter y setter mediante el menú Refactor class > Accesors > Accept.
Vemos cómo es la convención de getters y setters en Smalltalk:
count
^ count
count: anObject
count := anObject
Definimos el método renderContentOn:
renderContentOn: html
html text: self count asString.
html break.
html anchor
callback: [self answer];
with: 'Volver'
Y desde el componente anterior (WAContador) hacemos que el link nos muestre el Contador:
renderContentOn: html
html text: html class name.
html break.
html anchor
callback: [self call: CIUContador new];
with: 'Contador'
Ahora desde el componente inicial, ya podemos ir a nuestro nuevo componente.
Pero como no inicializamos el atributo count, se ve "nil" como contenido del count. A diferencia del null de Java, nil sí es un objeto y pertenece a la clase UndefinedObject. Todas las referencias por defecto apuntan a nil hasta que lo modifiquemos vía un setter o un initialize (que es llamado por el mensaje new de las clases):
initialize
count := 0.
Ahora sí vemos un hermoso... ¡ups! la aplicación tiró un error: veamos qué dice
MessageNotUnderstood: receiver of "contents" is nil
En este caso el problema fue que pisamos la definición de initialize de WAComponent, que deja el atributo "contents" apuntando al objeto que le corresponde. En ese caso, lo solucionamos como nos sugiere Seaside, enviando un mensaje super initialize primero (para que las superclases seteen las variables que quieran):
initialize
super initialize.
count := 0.
Peero, cada vez que vamos y volvemos se instancia un nuevo componente, porque estamos haciendo CIUContador new.
Entonces, para modificar el contador vamos a completar el nuevo componente con dos nuevos links para incrementar y decrementar el contador.
En la clase CIUContador cambiamos la definición del renderContentOn:
renderContentOn: html
html anchor callback: [self incrementar]; with: '++'.
html text: self count asString.
html anchor callback: [self decrementar]; with: '--'.
html break.
html anchor
callback: [self answer];
with: 'Volver'
Y definimos los métodos para incrementar y decrementar el contador:
incrementar
count := count + 1.
decrementar
count := count - 1.
Ahora con este cambio, vemos que, al igual en Wicket, Seaside maneja transparente, automática y mágicamente el request del browser al server y se encarga de llamar a nuestro bloque que le pasamos como "callback" (y aquí vemos en la práctica la utilidad de tener objetos bloque para construir interfaces de usuario).
En nuestro ejemplo invocamos los métodos incrementar/decrementar que modifica el estado (la variable count). Y Seaside simplemente vuelve a mostrar la misma instancia del componente.
Pregunta para el lector: ¿cuál es el modelo de esa ventana? ¿qué otras opciones tenemos para modelarlo?
Vimos que ahora con el comportamiento de "incrementar/decrementar" estamos manteniendo estado entre los diferentes requests. Ahora, ¿ qué pasa si empezamos a usar el botón "back" del navegador ?
¡Cosas raras!
Porque el navegador no vuelve a pedir la página anterior, sino que muestra lo que él se acuerda (caché). Esto hace que lo que vemos en el browser esté desincronizado con el estado en el server (la instancia del control). Para solucionar eso agregamos un método "states" al componente.
states
^Array with: self
Este método nos permite devolver un array con objetos a recordar "por seaside", cuando alguien "va para atrás". En este caso hace que se acuerde de todo el objeto.
Vimos que este problema es muy conocido en el desarrollo web. Y que existen diferentes técnicas para resolverlo, como [tratar] de invalidar la caché del browser, etc. Pero para quienes trabajen en Seaside el concepto de caché dependiente de la tecnología es transparente: el programador se concentra en una abstracción (el estado) que esconde los detalles de implementación internos.