Seaside - Desarrollo de componentes

Primer componente

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.

Ejemplo más Interesante

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".

Modelo de Componentes

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

  • por el uso de las clases que nos da seaside y sus brushes,
  • y por la descripción de la pantalla en forma programática. Esta idea es similar a la que hemos visto para Arena.

¿Qué otras formas tenemos para modelar la vista?

  • en forma visual
  • modelos basados en "transformaciones" como el templating, que es la evaluación de pedacitos de html con variables que se reemplazan luego, como en Grails/JSP.
  • o mediante tecnologías bastante más declarativas: XML, el mismo HTML que modela un documento, o bien trabajando con un DSL, un lenguaje para definir los widgets de una pantalla.

Callbacks y estado conversacional

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

  • El getter tiene el mismo nombre que la propiedad
  • El setter lleva el nombre de la propiedad + los dos puntos ":" que permite pasarle un argumento.
  • No hay prefijos get ni set, así es el default smalltalkero

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

Debug Proceed Full Stack

Possible Causes

  • you forgot to send "super initialize" in a initialize method of a component or task
  • the receiver of the message is nil
  • a class extension hasn't been loaded correctly
  • you sent the wrong message

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?

El "Odioso" Problema del Back Button

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.