MVC en Arena: Conversor
Pueden ver una presentación Prezi sobre MVC y Arena en este link. Es el material utilizado para la evaluación docente en el marco del concurso docente por el cargo de la materia en la Universidad de Quilmes, del 23/09/2014.
Primera app 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í:
Hacemos en la misma clase un main para poder ejecutarlo:
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 propiedadmillas
- El
Button
puede asociarse al método convertir - Y el
Label
muestra el valor de la propiedadkilometros
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.
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. 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:
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:
El binding lo establecemos mediante un operador <=> propio de Xtend, que logramos mediante este import:
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.
Afortunadamente los controles definen setters con formato de builder para poder encadenar los mensajes:
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 propiedadmillas
. - Al convertir, los cambios en el valor de la propiedad
kilometros
se reflejan en elLabel
.
Lo vemos gráficamente:
Más adelante vamos a ver ejemplos más complicados de binding.
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
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). Como 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"...
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.
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:
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.
Y las conversiones:
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, en el ConversorGenerico:
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:
Lo "nuevo" es la parte en que creamos el List. Con este código ya tendríamos una versión funcionando. 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
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.