Desarrollo de UI con componentes: Arena. Panel de edición.


Alta de un cliente de una empresa de celulares

Tenemos el enunciado de los clientes de una empresa de Celulares. Hablamos un poco de la pantalla de alta de un cliente.
 
¿Cuál es el modelo de la pantalla de Alta? Pareciera que un objeto celular. Entonces en este caso el modelo de la vista es un objeto de negocio / de dominio. El mapeo es casi directo entre control y propiedad, pero no siempre va a ser así (de hecho en los ejemplos que siguen vamos a jugar con modelos de vista más complejos, al menos no tan lineales).

El tema es: ¿y los botones? ¿Qué debe hacer el botón Aceptar? 

Bueno, básicamente tiene que agregar el cliente a la lista de clientes de la compañía. Esto es algo que no puede hacer el objeto Celular, lo que amerita una pequeña disgresión:

Algo más de arquitectura

En los ejemplos que vamos a ver a partir de ahora, vamos a intentar hacer aplicaciones más "reales". Eso implica pensar en que los objetos tienen que almacenarse en algún medio persistente. A nosotros no nos va a interesar "cómo funciona" la persistencia, pero sí cómo interactuamos con ella desde el resto de la aplicación (porque para hacer una aplicación completa necesitamos tener alguna noción de este aspecto).

Entonces agregar el cliente a la lista es simplemente enviar un mensaje a un objeto RepoCelulares que se encargue de esto:

override executeTask() {
    repoCelulares.create(modelObject)
    super.executeTask()
}


Para profundizar más en estos conceptos puede verse el artículo de los repositorios o homes.

Implementando la ventana

De la implementación de esa ventana podemos ver primero un par de detalles técnicos: Arena trabaja con mecanismos de reutilización mediante template methods, entonces deja los siguientes métodos para definir las diferentes porciones de la pantalla de edición:
  • en el createMainTemplate podemos definir el título de la pantalla (y a futuro, modificar el template de la pantalla general)
  • en el createFormPanel armamos el formulario propiamente dicho (los controles). Aquí utilizamos un layout de 2 columnas. Esto implica que cada control se ubica en la siguiente columna disponible (el tercer control va en la segunda fila y primera columna). 
    • Como el tamaño por defecto de los controles es muy chico lo seteamos a mano.
  • en el addActions definimos qué acciones puede disparar el usuario (los botones aceptar y cancelar). Aquí el layout es Horizontal, para que todos los botones se ubiquen uno al lado del otro en una sola fila.
  • para más información pueden ver el artículo específico que habla sobre los Layouts en Arena.
Otra cosa interesante es ver el diseño, que no es muy complejo pero nos interesa remarcar: vale diseñar en la UI [2]. En particular estamos acá usando un diseño muy simple, la ventana de creación extiende de la de edición porque comparten el layout y los controles, sólo se diferencian en dos comportamientos:
  • al crear, la ventana de edición recibe como modelo de la vista un elemento existente, en el caso de la acción nuevo se crea un nuevo objeto celular
  • al aceptar, el mensaje que le envío al repo puede ser create() o update() dependiendo de si es un alta o una modificación respectivamente
Otra alternativa que pueden ver en el ejemplo del Videoclub es tener una superclase abstracta y definir dos subclases para el Alta y la Modificación y manejarnos con template methods para que las subclases definan comportamiento específico.

[2] recordemos que diseñar es referido a diseño de sistemas

Repaso de binding

  • Los labels tienen un valor fijo en lugar de un binding (el modelo del label Nombre es el string "Nombre" que es inmutable)
  • El panel de edición tiene como model el celular pero además tiene que conocer al repositorio que le permite dar de alta el cliente
    • otra opción podría consistir en definir un objeto que modele el caso de uso Edición y que sea él el que conozca al repo. La vista delegaría el alta o la modificación a ese objeto. 
  • El botón default se asocia a un método accept() que ya está definido en el Dialog. Nosotros en la vista de edición solo tenemos que redefinir el método executeTask() -que es al que llama el método accept()- para indicar qué hace el botón aceptar. Es importante hacer super.executeTask() para marcarle a la pantalla madre que los cambios fueron aceptados por el usuario (de lo contrario los cambios que hagamos se perderán).

Acciones

Los dos botones invocan métodos sobre el dialog y no sobre el modelo. ¿Qué hacen la acción aceptar? Dos cosas
  • Inserta el objeto (necesidad del negocio)
  • Cierra la ventana (relacionado con la vista)
Es importante destacar que ni la vista ni el controller (listener) deben tener lógica del modelo, en todo caso lo que hacen es delegar al modelo para que él haga su trabajo. Una variante sería tener una acción de modelo que dispara un evento y la vista lo escucha.

Ventana de Dialog, controles y algo más sobre binding

1- Extendemos CrearCelularWindow / EditarCelularWindow de Dialog. 
  • el Dialog funciona como una ventana modal, esto es: es una vista que requiere que el usuario ingrese cierta información para continuar con el flujo del sistema y hasta que no complete el formulario, la aplicación no pueda continuar.
  • La ventana de edición tiene dos áreas
    • formulario: container (composite) donde van los controles
    • botonera de acciones
2- Para armar el formulario, aprovecha el template method de la superclase (SimpleWindow, padre de Dialog):

    override createFormPanel(Panel mainPanel) {
        val form = new Panel(mainPanel).layout = new ColumnLayout(2)
         
        new Label(form).text = "Número"
        new NumericField(form) => [
            value <=> "numero"
            width = 100

Algunas cosas:
  • new Label le pasa el form que acaban de crear. ¿Por qué era esto? Porque el form es un container de controles visuales
  • como el control queda agregado al formulario a partir de su creación, no necesitamos tener una variable para el label, ni para el TextBox. 
  • al enviar el mensaje bindValueToProperty("numero") o value <=> "numero" construimos un objeto que hace de binder, es un controller que permite que modelo y vista estén actualizados.
Así, cuando el usuario escribe algo en el textbox, se modifica el valor de la propiedad text y eso dispara la actualización del modelo: 
celular.setNombre con el valor escrito por el usuario (binding de la vista hacia el modelo).

Por otra parte, ¿cómo se actualizan los valores de cada propiedad de un celular? 
  • Con Reflection, el controller que construimos recibe las notificaciones de la vista y del modelo. Cuando son de la vista, toma el valor de la propiedad de la vista y dispara el mensaje modelo.setXXX(), cuando el modelo le avisa que una propiedad cambió, dispara un modelo.getXXX() y lo asocia a una propiedad de la vista.
  • Por eso hay un contrato que debemos respetar: la propiedad define
    • un getter para poder mostrar el valor en el panel
    • y un setter si vamos a querer modificarlo: si queremos editar el nombre para que el textbox pueda enviarle el mensaje setNombre el setter tiene que existir y la propiedad tiene que ser "nombre", no puede ser "nombre1" o "miNombre". Por el contrario si sólo nos interesa mostrar un Label con cierta información, no necesitamos tener un setter para esa propiedad.
    • en xtend el par getter/setter está implícito si yo le pongo la annotation @Accessor a una variable (o @Accessors a la clase), la transformo en propiedad y entonces hacer objeto.propiedad = unValor es un syntactic sugar de objeto.setPropiedad(unValor). 
    • en groovy ni siquiera necesito escribir la annotation.
  • Igualmente si hay algo mal definido recién salta en Runtime: al usar Reflection pierdo el chequeo en tiempo de compilación. 

Manejo de una Transacción

Un aspecto interesante con este fomulario es que queremos que trabaje dentro de una transacciónSeguimos con el formulario de edición

        new Selector<Modelo>(form) => [
            allowNull(false)
            value <=> "modeloCelular"
            val propiedadModelos = bindItems(new ObservableProperty(repoModelos, "modelos"))
            propiedadModelos.adaptWith(typeof(Modelo), "descripcionEntera") // opción A
            //propiedadModelos.adapter = new PropertyAdapter(typeof(Modelo), "descripcionEntera") // opción B
        ]

  • El combo de modelos necesita:
    • la lista de opciones, que es una colección de objetos y surge de un repo donde viven todos los modelos de celular disponibles para la aplicación 
    • y por otra parte, queremos definir qué vamos a mostrarle al usuario cuando vea cada opción del combo. Esto será una propiedad readOnly llamada "descripcionEntera", que se asocia al método getDescripcionEntera() en la clase Modelo.

Para leer en casa

Importante: para el TP

Si te interesa conocer más en profundidad la implementación del panel de Creación de un Celular/Socio (o estás buscando ayuda para el TP) podés ingresar a esta página.