Material‎ > ‎Software‎ > ‎Arena‎ > ‎

Arena - Bindings y demás controllers

Arena viene por defecto con mecanismos de binding que permiten mapear propiedades de vista con propiedades del modelo. No obstante, para ciertos casos especiales es útil utilizar o extender estos mecanismos de binding

Los componentes que vemos aquí están en el package org.uqbar.arena.bindings (del jar arena-core), salvo cuando se especifique otro.

Cómo funciona el Binding en Arena

El objeto Binding es una abstracción que vincula dos propiedades observables: 
  • una del modelo 
  • y otra de la vista. 
El binding se completa con un Transformer que permite ajustar las diferencias entre los valores manejados por modelo y vista.


Binders

ObservableProperty<T>

Implementación default del binding, esta clase escucha los eventos que dispara el cambio de una propiedad en el modelo.
Ejemplo de una asignación manual del Observable Property:

new Selector(editorPanel) => [
      allowNull = false
      bindItems(new ObservableProperty(this, "tiposPosibles"))
      ...

En este caso se permite modificar el modelo observado para que no sea el default de la vista (el panel principal).

De lo contrario se puede utilizar el constructor con un solo parámetro (la propiedad observada del modelo de la vista, que es el que se toma por defecto):

new Selector(editorPanel) => [
   allowNull = false
   bindItems(new ObservableProperty("jugadores"))
]

O directamente usamos un factory method:

new Selector(editorPanel) => [
   allowNull = false
   bindItemsToProperty("jugadores")
]

NotNullObservable

En esta implementación se escucha la propiedad de un modelo y se asocia a un valor booleano que la vista necesita (por ejemplo: enabled o visible), en base a que la propiedad tenga un valor o esté nula (true/false, respectivamente). 

// Deshabilitar los botones si no hay ningún elemento seleccionado en la grilla.
val elementSelected = new NotNullObservable("celularSeleccionado")
remove.bindEnabled(elementSelected)
edit.bindEnabled(elementSelected)

¿Qué necesito para generar nuevos observables?

En general estas dos implementaciones son suficientes para trabajar con Arena. No obstante, si lo necesitás podés crear tu propio ObservableProperty que escuche cambios en el modelo y los dispare a algún tipo de propiedad en la vista, definiendo estos métodos:
  • el constructor
  • configure(BindingBuilder binder): define un binding contra una propiedad, el binder se puede adaptar para que trabaje con un determinado transformer. Ejemplo de NotNullObservable:
@Override
public void configure(BindingBuilder binder) {
      super.configure(binder);
      binder.adaptWith(new NotNullTransformer());
}

ValueTransformer<M, V>

Interfaz que define cómo convertir y validar valores de la vista al modelo y viceversa (recordemos que el binding entre modelo y vista es bidireccional). Define esta interfaz:
  • M viewToModel(V valueFromView): convierte el valor de la vista al formato que el modelo necesita
  • V modelToView(M valueFromModel): convierte el valor del modelo al formato que la vista necesita
  • Class<M> getModelType(): devuelve la clase asociada al modelo
  • Class<V> getViewType(): devuelve la clase asociada a la vista

DateTransformer

Implementa ValueTransformer<Date, String>
Convierte a Date los Strings en formato dd/MM/yyy. Acepta valores nulos.
Ejemplo de uso: creamos un textbox que ingresa una propiedad fechaDesde definida como Date en el modelo: 

// Xtend
new TextBox(mainPanel) => [
      width = 80
      value <=> "fechaDesde".transformer = new DateTransformer
]

// Java
new TextBox(mainPanel)
      .setWidth(80)
      .bindValueToProperty("fechaDesde").setTransformer(new DateTransformer());

NotEmptyTransformer

Se ubica en el package org.uqbar.lacar.ui.model.transformer
Implementa ValueTransformer<String, Boolean>
Mapea un objeto String a un booleano que indica si el string es nulo o vacío.

NotNullTransformer

Se ubica en el package org.uqbar.lacar.ui.model.transformer
Implementa ValueTransformer<Object, Boolean>
Mapea cualquier Object con un valor booleano que indica si es distinto de null.

Quiero definir un nuevo Transformer, ¿qué necesito?

  • Definir las clases M y V para el modelo y la vista, respectivamente
  • implementar ValueTransformer de esas clases concretas M y V
  • definir los métodos getModelType() y getViewType() en base a las clases concretas
  • definir los métodos viewToModel() y modelToView() para indicar cómo se debe adaptar el valor de la vista al modelo y viceversa

Ejemplo: BigDecimalTransformer

Esta clase forma parte del ejemplo de las apuestas:
class BigDecimalTransformer implements ValueTransformer<BigDecimal, String> {
      override getModelType() {
            BigDecimal
      }
 
      override getViewType() {
            String
      }
 
      override modelToView(BigDecimal valueFromModel) {
            valueFromModel.toString
      }
 
      override viewToModel(String valueFromView) {
            if (valueFromView == null || valueFromView.equals(""))
                  null
            else
                  try
                        new BigDecimal(valueFromView)
                  catch (NumberFormatException e)
                        throw new UserException("El valor ingresado debe ser un número", e)
 
      }
}

TextFilter

Se ubica en el package org.uqbar.arena.widgets. Permite interceptar valores ingresados a un campo textbox.
La interfaz define un solo método:
  • override boolean accept(TextInputEvent event): aquí hay que indicar con true/false si se acepta el caracter ingresado por teclado. 
Ejemplo 1: DateTextFilter
class DateTextFilter implements TextFilter {
     
      override accept(TextInputEvent event) {
            val expected = new ArrayList(#["\\d", "\\d?", "/", "\\d", "\\d?", "/", "\\d{0,4}"])
            val regex = expected.reverse.fold("")[result, element| '''(«element»«result»)?''']
            event.potentialTextResult.matches(regex)
      }
     
}


Ejemplo 2: un filtro que sólo permite números, comas y puntos decimales (el que usa el campo NumericField)

class NumeroFilter implements TextFilter {
    
    override accept(TextInputEvent event) {
        event.potentialTextResult.matches("[0-9,.]*")
    }
    
}

Se usa de la siguiente manera.
// Java
new TextBox(editorPanel)
    .withFilter(new DateTextFilter)
    .bindValueToProperty("millas");


// Xtend
new TextBox(editorPanel) => [
    withFilter(new DateTextFilter)

Adapters

Property Adapter

Los selectores o combos necesitan trabajar con un objeto Adapter, para poder mostrar adecuadamente los elementos que contienen:
// Xtend
new Selector<Modelo>(form) => [
    allowNull(false)
    value <=> "modeloCelular"
    val bindingItems = bindItems(new ObservableProperty(repoModelos, "modelos"))
    bindingItems.adaptWith(typeof(Modelo), "descripcionEntera") // opción A
    //bindingItems.adapter = new PropertyAdapter(typeof(Modelo), "descripcionEntera") // opción B
]

// Java
Selector<ModeloCelular> selector = new Selector<ModeloCelular>(form) //
   .allowNull(false);
selector.bindValueToProperty("modeloCelular");
Binding<ModeloCelular, Selector<ModeloCelular>, ListBuilder<ModeloCelular>> itemsBinding = selector.bindItems( //
   new ObservableProperty<ModeloCelular>(getRepoModelos(), "modelos"));
itemsBinding.setAdapter( //
   new PropertyAdapter(ModeloCelular.class, "descripcionEntera"));

Lo que hacemos en el ejemplo de arriba es definir un selector o combo que muestra objetos Modelo de celular. Pero ¿qué propiedad usará para mostrar cada uno de los modelos de celular disponibles? Ahí es donde le decimos que utilice un adapter contra la propiedad descripcionEntera, definida en la clase Modelo:
def getDescripcionEntera() {
      descripcion.concat(" ($ ").concat(costo.toString).concat(")")
}

Eso produce que en el combo se muestre la descripción del modelo y su precio en $:


Transformers para columnas de una grilla

Las tablas o grillas tienen en cada columna un binding con la propiedad que están observando:
new Column<Celular>(table) //
      .setTitle("Nombre")
      .setFixedSize(150)
      .bindContentsToProperty("nombre")

No obstante, en algunos casos, es necesario adaptar la vista para mejorar la información que visualiza el usuario:

        new Column<Celular>(table) => [ 
                bindContentsToProperty("recibeResumenCuenta").transformer =
                        [ Boolean recibe | if (recibe) "SI" else "NO" ]
                ...  
        ]

Aquí vemos que en lugar de mostrar "true" o "false", adaptamos el valor booleano a un string "SI"o "NO" respectivo:



Este otro ejemplo muestra cómo mostrar una columna con colores diferentes, donde true implica verde y false implica rojo:

        new Column<Celular>(table) => [
            bindBackground("recibeResumenCuenta").transformer =
                [ Boolean recibe | if (recibe) Color.GREEN else Color.RED ]
            ...            
        ]

Y se visualiza de esta manera:

La interfaz que estamos implementando es com.uqbar.commons.collections.Tranformer<T,U> (del package  uqbar-commons-collections) :
public interface Transformer<T, U> {
      public U transform(T element);
}
Comments