MVC en Arena: Conversor











Construir nuestra primera aplicación siguiendo el patrón MVC

A. La vista

Nos basamos en un ejemplo muy sencillito: una aplicación que convierte millas a kilómetros. Ingresás una distancia en millas, presionás un botón convertir y te la convierte a kilómetros. Dado que vamos a trabajar con interfaces orientadas a objetos, comenzamos pensando qué objetos intervienen en esa interfaz. Rápidamente surgen cuatro objetos visuales:
  • Una caja de texto o textbox numérico que nos permite ingresar una distancia en millas.
  • Un botón que dispara la conversión.
  • Un label que muestra el resultado.
  • Una ventana que contiene a todos los demás.
Si bien son menos obvios, nos dimos cuenta de que estamos usando dos más:
  • Un panel, que agrupa a los otros tres componentes. En realidad entonces la ventana no contiene a los componentes sino únicamente al panel, y éste a los componentes. Esto es por una cuestión técnica, en el framework que vamos a usar, las ventanas tienen un panel principal y los demás componentes se agregan a este panel, en otras tecnologías podrían agregarse directamente a la ventana sin necesidad del intermediario.
  • Un layout, que es el que tiene la responsabilidad de "acomodar" los componentes dentro del panel que los contiene. Más adelante vamos a ver qué tipos de layout hay.
Construimos esos objetos en Arena, para eso hicimos una clase que extendiera de MainWindow; eso nos obliga a implementar el método createContents, que describe el contenido de la ventana, y nos quedó así:

override createContents(Panel mainPanel) {
    this.setTitle("Conversor de millas a kilómetros (XTend)")
    mainPanel.setLayout(new VerticalLayout)
    new Label(mainPanel).text = "Ingrese la longitud en millas"
    new NumericField(mainPanel) => [
        value <=> "millas"
    ]
    new Button(mainPanel) => [
        caption = "Convertir a kilómetros"
        ...
    ] 
    new Label(mainPanel) => [
        background = Color.ORANGE
        value <=> "kilometros"
    ]
    new Label(mainPanel).text = "kilómetros"
}


Hacemos en la misma clase un main para poder ejecutarlo:


def static main(String[] args) {
    new ConversorWindow().startApplication
}

Una cosa a tener en cuenta es que en Arena es obligatorio definirle un layout a todos los paneles. También vimos que los componentes de Arena utilizan el patrón Composite.

B. El modelo

Nuestro modelo es muy sencillo: un objeto Converter con dos propiedades: millas y kilometros (con sus respectivos getters y setters) y un método convertir que toma el valor de millas, lo convierte y lo guarda en el atributo kilometros.

Estos son dos patrones que van a seguir muchos de nuestros objetos de negocio:
  • Las características de nuestros objetos que sean visibles por el usuario estarán representadas por pares getter/setter. Cuando nos referimos a un atributo de un objeto, pensamos en un par getter/setter, si eso tiene un field atrás no es importante para nosotros, nos interesa la interfaz y no la implementación.
    Un detalle es que a veces puede aparecer el getter solamente, esto denota un atributo inmutable (es sólo lectura para el usuario).
  • Las acciones de nuestros objetos que son ejecutables por el usuario estarán representadas (muchas veces) por métodos que no reciben parámetros y no devuelven nada. Esto puede resultar un poco restrictivo, pero simplifica mucho la relación entre dominio y vista; y toda la información que se necesite intercambiar se puede hacer casi siempre utilizando atributos.

C. El controller

Pasamos a diseñar el controller. Como verán, en este modelo muy simple hay una relación casi uno a uno entre los conceptos de la vista y los del modelo:
  • Tenemos una ventana para el Conversor
  • Un NumericField (especie de TextBox) para la propiedad millas
  • El Button puede asociarse al método convertir
  • Y el Label muestra el valor de la propiedad kilometros
En la medida de lo posible es una buena práctica mantener este tipo de asociaciones uno a uno entre vista y modelo, porque simplifican mucho establecer la relación entre ambos, que en muchos casos es la parte más complicada de la UI.

La forma preferida de vincular vista y modelo es a través de eventos. Tanto la vista como el modelo pueden tirar eventos y obviamente escuchar los eventos que tira el otro. El patrón detrás del concepto de evento es el observer (o listener) y lo que nos permite es tener una comunicación bidireccional sin producir un acoplamiento.

C1. Asociar un listener a un evento de la vista

El caso más fácil de evento es el que se produce al presionar el botón "convertir". Para asociarle un observer a ese evento utilizamos el mensaje onClick, que recibe algún objeto que implemente la interfaz Action.
La implementación de esa interfaz se debe hacer reificando la acción en un bloque de código o closure, que dice qué mensaje/s enviar cuando el usuario presiona el botón. 

new Button(mainPanel) => [
    ...
    onClick [ | this.modelObject.convertir ]
]

C2. Disparar eventos desde el modelo

Esta parte depende mucho de las posibilidades que den los distintos lenguajes para manejar eventos. En Xtend/Java utilizamos los eventos de java.beans, para más información vayan a la página de documentación de Manejo de Eventos en Java.

La forma más simple que tiene el framework Arena para disparar un evento ante la modificación de una propiedad de un objeto del dominio es

1. anotar la clase (en la definición) como un @Observable:
@Observable
class Conversor {

2. definir getters y/o setters para nuestros fields: en este caso, Conversor tiene millas y kilometros. En Java esa definición es explícita, en otros lenguajes tenemos shortcuts.

Diferencia entre fields y atributos: Llamamos field a la variable de instancia, es una cuestión de implementación. Por otro lado con atributo o propiedad nos referimos a un par de métodos getter/setter, es decir es una cuestión de interfaz

En este caso, como en muchos, la implementación del atributo es a través de un field con el mismo nombre, pero vamos a ver que eso no tiene por qué ser así.

C3. Binding

El siguiente paso sería poner listeners entre el TextBox (NumericField) y la propiedad millas. Lo que este listener tiene que hacer es bastante simple: escucha el evento que se produce al cambiar el valor del TextBox y en ese momento actualiza el atributo millas.

Como este tipo de listeners es muy común, existe un mecanismo de más alto nivel que lo hace más sencillo, denominado binding (es decir: vinculación).

El caso más simple de binding es el que vincula (o "bindea") una característica de la vista (por ejemplo el valor de un TextBox) con una propiedad del modelo. En Arena esto se indica de la siguiente manera:
    new NumericField(mainPanel) => [
        value <=> "millas"
    ]

El binding lo establecemos mediante un operador <=> propio de Xtend, que logramos mediante este import:

import static extension org.uqbar.arena.xtend.ArenaXtendExtensions.*


Para más información tenés una página especial dedicada a Arena en Xtend.


Si estás trabajando en Java, que no tiene este syntactic sugar, tenés que usar el mensaje bindValueToProperty. 

new NumericField(mainPanel).bindValueToProperty("millas");


Afortunadamente los controles definen setters con formato de builder para poder encadenar los mensajes:

        new Label(mainPanel) //
            .setBackground(Color.ORANGE)
            .bindValueToProperty("kilometros");

Por defecto el binding es bidireccional, es decir los cambios pueden provenir tanto del modelo como de la vista y ambos valores se van a mantener sincronizados. En nuestro ejemplo vemos ambos tipos de comunicación:
  • Los cambios en el valor del NumericField se propagan a la propiedad millas.
  • Al convertir, los cambios en el valor de la propiedad kilometros se reflejan en el Label.
Lo vemos gráficamente:

Más adelante vamos a ver ejemplos más complicados de binding.
Antes de olvidarnos, los ejemplos de Arena necesitan una pequeña configuración para que funcionen adecuadamente.

Variantes del conversor

Veamos cómo encarar algunos cambios en la interacción y funcionalidad de nuestro conversor.
A diferencia de otros frameworks de más bajo nivel de UI, Arena se basa en estos conceptos principales de MVC y binding que ya vimos. Y el lugar principal que tenemos como desarrolladores para modelar, va a ser el Modelo, en lugar de el Controller o la Vista.

Conversor Sin Botón (automático al tipear)

Queremos evitar el botón, para que la interfaz sea más "ágil". Es decir que la conversión se haga automáticamente a medida que se ingresan las millas.
Lo primero que hacemos es, remover el botón de la vista.

Borramos estas lineas
new Button(mainPanel) => [
       caption = "Convertir a kilómetros"
       onClick [|this.modelObject.convertir]
]

Y así queda nuestro diagrama:
Si lo ejecutamos vamos a ver que si bien se actualizan las millas, no se ven los kilómetros.
Esto es porque ya nadie llama al "convertir()" como podemos ver con la flecha "?" (que en realidad no existe).
Cómo está el modelo hoy en día, quien modifica los kilómetros es el método convertir. Pero nadie lo llama.

Entonces, ¿ qué hacemos en Arena para lograr el efecto ?
En otros frameworks uno estaría tentado a buscar un nuevo listener en los TextBoxes, y ahí escribir código en ese controller.
En Arena en cambio, se fomenta la idea de programar en el modelo.

Entonces, si pensamos en abstracto el problema, lo que necesitamos es que "al ingresar millas, se calculen automáticamente los kilómetros"
¿ Cómo se haría eso en el modelo ?

La respuesta es agregar comportamiento al setter de millas. Cuando se llama al setMillas, vamos a invocar al "convertir"
def void setMillas(double millas) {
       this.millas = millas
       this.convertir()
}

Y ahora sí ya funciona automáticamente:

Fíjense que el comportamiento, en cuanto a interacción de la pantalla, tiene en realidad una correspondencia con el modelo subyacente.
Esa es una característica de Arena y de MVC. Es importante hacer este "click" para empezar a pensar la forma de construir interfaces con Arena basándonos en el modelo y no por el contrario en la vista.

Conversor Sincronizado en ambos sentidos

Segundo caso. Continuando con el ejemplo anterior "sin botón", queremos ahora implementar un cambio de modo de poder convertir en ambos sentidos. Es decir, que al ingresar millas, calcule los kilómetros, pero además, que al ingresar kilómetros, calcule las millas.
De nuevo, ¿ cómo hacemos eso ?
Lo primero sería cambiar el componente de UI para el "kilometros", en lugar de un Label, utilizar un NumericField.
new NumericField(mainPanel).value <=> "kilometros"

Ahora, además, necesitamos modificar el modelo. Porque ya vimos que al ingresar las millas, se invoca a la conversión, pero al ingresar los kilómetros no se calcula nada. Así que podemos proceder de la misma forma e implementar el cálculo en el setter de kilometros:
@Observable
@Accessors
class ConversorSincronizado {
       double millas
       double kilometros
      
       def void setMillas(double millas) {
             this.millas = millas
             this.kilometros = millas * 1.60934
       }
      
       def void setKilometros(double kilometros) {
             this.kilometros = kilometros
             this.millas = kilometros / 1.60934
       }
}


Listo, con esto ya tenemos nuestro nuevo conversor sincronizado.



Conversor Genérico con Unidades

Vamos a volver un poco sobre nuestros pasos, recuperamos el botón. Lo que queremos hacer ahora es tener un conversor más inteligente, que sepa convertir entre diferentes unidades:
  • millas -> km
  • km -> millas
  • onzas -> gramos
  • gramos -> onzas
Como en Arena decimos que nos centramos en el dominio, esto quiere decir que lo primero y principal que tenemos que hacer es modelar esto en el Conversor.
Acá queda bien la idea de un Strategy. Entonces el conversor no sabe convertir, sino que delega en un objeto de tipo Conversion.
Acá vemos el código xtend.
@Observable
@Accessors
class ConversorGenerico {
       @Property double input
       @Property double output
       @Property Conversion conversion
 
       def void convertir() {
             output = conversion.convertir(this.input)
       }
...
}

Y las conversiones:
@Observable
abstract class Conversion {
       def double convertir(double input)
       def String getNombre()
}
 
class MillasAKmConversion extends Conversion {
       override convertir(double input) { return input * 1.60934 }
       override getNombre() { "millas -> km" }
}
 
class KmAMillasConversion extends Conversion {
       override convertir(double input) { return input / 1.60934 }
       override getNombre() { "km -> millas" }
}
 
class OnzaAGramosConversion extends Conversion {
       override convertir(double input) { return input * 28.3495231 }
       override getNombre() { "onza -> gr" }
}
 
class GramosAOnzaConversion extends Conversion {
       override convertir(double input) { return input / 28.3495231 }
       override getNombre() { "gr -> onza" }
}

Ahora sí podríamos incluso hacer un test independiente de la UI. Nuestro conversor permite cambiarle la "Conversion" ya que es una propiedad.
Entonces, planteamos nuestra vista así:


Acá aparece un nuevo control en la vista, ya que necesitamos que el usuario elija un objeto de tipo Conversión. A esto le llamamos "selección". Se selecciona un objeto de entre una lista de objetos.
En realidad hay varios componentes de vista que podrían servir para esto, en Arena podemos nombrar dos:
  • Selector:  es lo que comúnmente llamamos "combo" o lista desplegable.
  • List: es muy similar al Selector, pero no es desplegable, es decir que siempre se ve toda la lista, y podemos elegir un elemento de ella (es el que se ve en el dibujo que ya mostramos aquí arriba).
Ambos controles cumplen una función similar a al Textbox, respecto de su relación con el modelo.
Es decir que  se bindean contra una propiedad del modelo.

¿Qué quiere decir esto? que cuando el usuario seleccione un objeto, éste se va a utilizar como parámetro para invocar al setter.
Además, recordamos que el binding es bidireccional, con lo que si cambia el valor de la property, automáticamente se va a actualizar el seleccionado en la vista.

A diferencia de los TextBox o Label, el Selector o List, tienen además un segundo binding que se utiliza para vincularse a una propiedad que debe ser de tipo Collection, ya que esta propiedad es la que determina los valores posibles u "opciones" que se le presenta al usuario para que seleccione.
Entonces vamos a actualizar el dibujo. Algún objeto tendrá que tener un "getConversionesPosibles" es decir la responsabilidad de darnos la lista de posibles Conversiones.


Ahí marcamos en azul los bindings sobre el "valor" de los componentes. En cambio el List, tiene dos bindings, uno para el valor (seleccionado) y otro para los items u opciones.

El código quedaría así.
En el ConversorGenerico:
def getConversionesPosibles() {
    #[  new MillasAKmConversion,
            new KmAMillasConversion,
            new OnzaAGramosConversion,
            new GramosAOnzaConversion
    ]
}

Fíjense que ni siquiera es una variable de instancia. Es un solo un getter que retorna una lista que crea cada vez.

La vista, es decir la ventana queda:
override createContents(Panel mainPanel) {
       this.title = "Conversor generico (XTend)"
      
       new Label(mainPanel).text = "De:"
 
       new NumericField(mainPanel).value <=> "input"
      
       new List(mainPanel) => [
             items <=> "conversionesPosibles"
             value <=> "conversion"
       ]
 
       new Button(mainPanel) => [
             caption = "Convertir"
             onClick [ | modelObject.convertir ]
       ]
      
       new Label(mainPanel).setText("A: ")    
       new Label(mainPanel) => [
             background = Color.ORANGE
             value <=> "output"
       ]  
}

Lo "nuevo" es la parte en que creamos el List.
Con este código ya tendríamos una versión funcional.

El problema es que verán que las opciones de Conversiones, serán strings raros como "Conversion@231a3e2f". Esto es porque el List por defecto le hace un toString a los objetos para mostrarlos en pantalla.
Para arreglar esto podríamos redefinir el toString de Conversion para que retornen el nombre.
O bien, configurar un aspecto del binding, a través de un objeto de tipo Adapter, que permitirá transformar los objetos a String
def nombreAdapter() {
    new PropertyAdapter(typeof(Conversion), "nombre")
}

...
    new List(mainPanel) => [
        allowNull(false)
        (items <=> "conversionesPosibles").adapter = nombreAdapter
        value <=> "conversion"
    ]

En este caso estamos usando un PropertyAdapter, que ya viene en Arena que permite especificar el nombre de una propiedad de los objetos que queremos que use para mostrarlos como opciones.


En el ejemplo que podés descargar tenemos además manejo de errores y otras funcionalidades como que el botón de convertir solo se habilita si se selección una conversión, para evitar una exception, etc. Pero ya es tema para otro apunte.