Desarrollo de UI con componentes: Panel de búsqueda

Tomamos como base el ejemplo de los celulares

Una vista un poco más compleja: pantalla de búsqueda

La pantalla de búsqueda del ejemplo de los celulares la pensamos como un panel dividido en las siguientes secciones:
  • feedback/error: un container que permite mostrar los errores que ocurran en la carga de datos del panel, por ejemplo al buscar un número que tenga caracteres alfabéticos.
  • form: un formulario que permite establecer un criterio de búsqueda (en principio por número o nombre). 
  • actions: una botonera de acciones asociadas al formulario de búsqueda que definimos anteriormente: 
    • Buscar dispara la búsqueda en base a los criterios ingresados por el usuario, 
    • Limpiar construye un nuevo criterio de búsqueda vacío y 
    • Nuevo permite abrir una ventana de edición de un nuevo celular
  • grid results: un panel que contiene una grilla que permite mostrar el conjunto de celulares encontrado en base al criterio de búsqueda
  • grid actions: una botonera asociada a cada elemento seleccionado de la grilla: podemos 
    • Editar los datos del celular o 
    • Eliminar el celular

La vista

  • El createContents() tiene que crear varios paneles, nuestra vista puede heredar de Window o bien de SimpleWindow...
  • SimpleWindow implementa el createContents creando
    • un error panel (veremos después para qué sirve)
    • un form panel (donde va un formulario con controles)
    • un action panel (donde van los botones)

>>SimpleWindow
    @Override
    public void createContents(Panel mainPanel) {
        this.configureLayout(mainPanel);
        this.createMainTemplate(mainPanel);
    }
    protected void createMainTemplate(Panel mainPanel) {
        this.createErrorsPanel(mainPanel);
        this.createFormPanel(mainPanel);
        this.createActionsPanel(mainPanel);
    }
  • ¿nos sirve así como viene? No, porque falta la grilla y la botonera del elemento seleccionado en la grilla, entonces qué hacemos
    • redefinimos createContents(), y armamos los 5 paneles a mano (eso nos fuerza a repetir código)
    • redefinimos createContents(), pero aprovechando la definición original de SimpleWindow, e incorporando la grilla. De esta manera no repetimos ideas.
>>BuscarCelularesWindow
    override def createMainTemplate(Panel mainPanel) {
        title = "Buscador de Clientes"
        taskDescription = "Ingrese los parámetros de búsqueda"
        super.createMainTemplate(mainPanel)
        this.createResultsGrid(mainPanel)
        this.createGridActions(mainPanel)
    }

Modelo de la vista

¿Qué tenemos como modelo en esta pantalla?

El objeto detrás de esa pantalla es un "buscador", que tiene:
  • Dos propiedades que son los filtros de búsqueda (número y nombre).
  • Una propiedad que representa el resultado de la búsqueda (una lista).
  • Una propiedad que se asocia al celular seleccionado en la grilla
  • Un método que ejecuta la búsqueda, tomando los datos de los filtros y actualizando el resultado de forma adecuada. Esto podría hacerse de muchas maneras, en nuestro caso estamos utilizando una busqueda by example (que veremos luego).

Binding de propiedades "anidadas"

Un detalle técnico es que podríamos tener un objeto Celular contra el cual hacer el binding de los campos de filtrado. Las formas de resolverlo son:
  • El ejemplo de los celulares lo resuelve a nivel modelo, agregando una propiedad por cada campo que forma parte del criterio de búsqueda
  • Tener una propiedad "example" o "celularBusqueda" que sea de tipo celular. Entonces debemos
    • modificar el binding del panel de búsqueda para que el modelo sea ese example (la propiedad "example" del modelo de la vista general). Como consecuencia de esto los campos contenidos en él referencian a propiedades "nombre", "numero", etc. Así funciona el ejemplo del videoclub:

>>BuscarCelularesWindow
    @Override
    protected void createFormPanel(Panel mainPanel) {
          Panel searchFormPanel = new Panel(mainPanel);
          searchFormPanel.bindContents("example");     
               <== el modelo de la vista tiene una propiedad example
          searchFormPanel.setLayout(new ColumnLayout(2));
          Label nombreLabel = ...
          Control nombre = new TextBox(searchFormPanel);
          nombre.bindValueToProperty("nombre");     
                       <== el example tiene una propiedad nombre
                                                                                 
    • utilizar propiedades anidadas, de la forma "example.nombre". Esto es muy común en los frameworks de presentación en tecnología Java.
La preferencia por alguna estrategia por sobre otra depende de varios factores: si tenemos un criterio de búsqueda con muchos campos repetir cada una de las propiedades no sólo es tedioso sino que genera un doble trabajo cuando se incorporan nuevos campos. Más adelante hablaremos sobre cómo representar el criterio de búsqueda.

Layout
Cada una de las secciones utilizan un layout diferente:
  • las botoneras se ubican en un panel que actúa como contenedor de los controles button y que tiene un layout horizontal: cada control se ubica a la derecha del otro
  • el panel de búsqueda trabaja con un layout de 2 columnas: cada nuevo control que se define se ubica en la siguiente columna (izquierda, derecha, luego fila siguiente: izquierda, derecha, etc.)
  • la grilla no necesita un panel porque es el único control dentro de su sección
  • y por último, la vista en general tiene un layout vertical: el panel de feedback/error está arriba, más abajo se ubica el formulario de búsqueda, debajo la botonera de acciones, etc.
Para más información pueden ver el artículo específico que habla sobre los Layouts en Arena.

Tablas

Las tablas nos permiten mostrar muchos objetos del mismo tipo y seleccionar uno de ellos. Eso nos permite bindear dos propiedades de la tabla:
  • Sus contenidos, contra una colección del modelo
  • el valor actual, el objeto seleccionado
El valor de cada columna, a su vez, tiene binding contra una propiedad de cada uno de los elementos de esa colección. Esto nos permite trabajar con objetos polimórficos: basta con que implementen el mismo mensaje que define esa propiedad.

También podemos ver que los botones de la tabla se deshabilitan cuando no hay un objeto seleccionado, eso se logra con un binding especial (que se fija que la propiedad sea no nula).

¿Qué representa ese objeto "buscador"? Es el modelo de la ventana, sin embargo no puede decirse que sea un objeto de dominio como Celular, Socio o Película. Es un objeto que modela un caso de uso, o application model (pueden ver con más profundidad este artículo).

Esquema MMVC del panel de búsqueda

  • V - Vista: la pantalla BuscarCelularesWindow
  • C - Controller: adapta la vista con el modelo (en este caso con el application model), es el encargado de resolver el binding. Arena viene con muchos de ellos, y permite que nosotros generemos nuestros propios controllers
    • transformers: DateTransformer, NotEmptyTransformer, etc.
    • binders: NotNullObservable, ObservableProperty, etc.
    • filters: TextFilter
    • actions: las acciones de los botones, modelado con un Closure que implementa la interfaz IAction
  • M - Modelo de la vista/modelo de aplicación/application model: en este caso se implementa con un objeto BuscadorCelular, en el ejemplo del videoclub se trabaja con una instancia de SearchByExample aplicado a un <Socio>. El application model conoce a un repositorio y delega en él las acciones "buscar" y "borrar"
  • M - Modelo de dominio: el celular

Binding bidireccional: secuencia de pasos de una búsqueda

El botón Buscar se define en el addActions: 
     override protected addActions(Panel actionsPanel) {
          new Button(actionsPanel)
                .setCaption("Buscar")
                .onClick [ | modelObject.search ]
                .setAsDefault
                .disableOnError

Esto construye un binding entre el evento onClick y el envío de un mensaje "search" al objeto que sea model de nuestra pantalla de búsqueda. Entonces cuando el usuario presiona el botón "Buscar" se actualiza la lista de resultados del Buscador

    def void search() {
       ...
       resultados = getHomeCelulares().search(numero, nombre)
    }

Para que la vista se actualice, es fundamental que el BuscadorCelular tenga la annotation @Observable...

Esto produce la notificación a los que les interesa saber cuándo la propiedad resultados cambia su valor. Uno de esos interesados es la grilla, según lo definimos en el método createResultsGrid de la vista:

   table.items <=> "resultados"

Entonces solito se actualiza el resultado de la grilla, no hay que iterar con un for los resultados para llenar nada. 

La búsqueda es un lindo ejemplo de cómo funciona el binding bidireccional:
  • cuando el usuario presiona el botón Buscar el botón notifica que lo presionaron a sus interesados (es un Observer disimulado por el binding)
  • cuando se hace la búsqueda, se guarda ese resultado en el atrributo "resultados", entonces el modelo de la vista notifica a la grilla (vista) que su contenido cambió 
Vemos un diagrama de clases de la solución a grandes rasgos, para el ejemplo de los celulares:



(hacé click en la imagen para verla mejor) - en amarillo los packages desarrollados en el ejemplo de Celulares - el resto corresponden a Arena + Uqbar Domain.

Si te interesa revisar la solución del videoclub, que utiliza homes persistentes y un objeto search by example, hacé click aquí.

  • Las vistas aceptan como modelo un T que debe ser observable. 
  • Con respecto al application model 
    • si queremos usar un SearchByExample o un Search, trabajamos la búsqueda de T donde T debe ser una Entity (anotada como Observable). 
    • si lo definimos nosotros debemos definir ese objeto como Observable.
  • Por último, los homes almacenan objetos T, donde T debe ser una Entity (anotada como observable). 
    • Las Entity definen un identificador unívoco, así los homes ofrecen un searchById
El diagrama de secuencia muestra la interacción de los objetos en el tiempo:

En definitiva, si la aplicación está bien construida lo que pasa es:
  • un cambio en la vista dispara un mensaje a un objeto de negocio
  • el negocio modifica su estado interno y dispara la notificación a la vista (en realidad, a todos los interesados, la vista es sólo uno más)
  • la vista se actualiza con la información del objeto de negocio (esto puede ser que ahora un campo esté deshabilitado o que mostremos el socio en color azul porque ya no debe plata)

Un nuevo enfoque

La forma en que resolvimos la búsqueda de celulares difiere bastante de la manera "tradicional" en la que estamos acostumbrados a programar, que bien podría tener un formato como el siguiente:

@Override 
protected void onClick() { 
    //click del botón presionado por el usuario 
    int numero = txtNumeroCelularABuscar.getValue().intValue();
    String nombre = txtNombreCelularABuscar.getValue();
    List<Celular> resultados = HomeCelulares.getInstance().searchByExample(new Celular(numero, nombre));
    grillaCelulares.clear();
    for (Celular celular : resultados) {
        Row celularRow = grillaCelulares.addRow(4);
        celularRow.getColumn(0).setValue(celular.getNumero());
        celularRow.getColumn(1).setValue(celular.getNombre());
        ... agregamos el resto de los campos y si hay que convertir la info lo hacemos aquí ...
    }
}


Es decir, estamos acostumbrados a escribir el código que va en el botón, teniendo mayor control sobre el algoritmo.
Por el contrario nuestra propuesta trabaja con un grado mayor de indirección. 
Podemos hacer un cuadro comparativo de ambas soluciones:

 Código en el botón     Nuestra propuesta
 Todo el código está puesto en el botón, entonces es fácil saber qué hace de un primer vistazo.
 Requiere tiempo para poder tener una visión general de la solución
 Si queremos agregar más comportamiento en la búsqueda, debemos agregar más líneas en el método onClick() del botón - propio de la tecnología Agregar más comportamiento en la búsqueda implica escribir más líneas en el método search() - que no depende de la tecnología
 Tenemos mayor control sobre el algoritmo, podemos cambiar la forma en que accedemos a cada celular, agregar filtros, cambiar el ordenamiento, etc.     Sólo nos concentramos en definir qué contiene cada columna que queremos visualizar. Es más declarativo, escribo menos y tengo menos probabilidad de cometer errores.
 El binding es unidireccional: la vista siempre actualiza el modelo. Cuando el modelo cambia la vista muestra información desactualizada (no recibe notificaciones). El binding es bidireccional, la vista puede actualizar el modelo o viceversa. Esto permite que no tenga que preocuparme desde dónde se actualiza cada modelo, yo se que el modelo es responsable de notificar cada cambio que se produzca.
 Si en otra pantalla necesito disparar la búsqueda de celulares es difícil reutilizar el código que está en el onClick(), la vista accede directamente al Home/Repositorio, están mezcladas las cuestiones tecnológicas con la forma de bindear los resultados (hay menos cohesión)Facilita la abstracción al separar las responsabilidades de cada objeto: vista, application model, home/repositorio o DAO y objetos de negocio. En cada uno de los objetos tengo la posibilidad de extraer comportamiento común.
  En este tipo de soluciones cualquier cambio en la pantalla de búsqueda incrementa notablemente la cantidad de líneas del método onClick(). En programadores con baja experiencia esto suele ser una ventaja inicial, pero con el paso del tiempo y de las modificaciones la solución pierde robustez, escalabilidad y sobre todo, mantenibilidad. Esta solución necesita que pensemos bien quién es responsable de cada cambio:
  • si necesito agregar un campo de búsqueda, esto afecta tanto a la vista como al application model (y al repositorio, claro, porque es inevitable ese acoplamiento en el cambio de funcionalidad planteada)
  • si quiero buscar por número o nombre (en lugar de número y nombre), hay que modificar el comportamiento del repositorio
  • si quiero disparar la búsqueda cada vez que el usuario salga del campo número o nombre, sólo debería modificar el binding de la vista (application model y repositorio permanecen intactos)
Es justamente lo que nos propone objetos, tener cajones para cada cosa, si no somos ordenados los cajones no sirven para nada. Pero si respetamos lo que cada objeto debe hacer tendremos a nuestro favor un diseño que nos permita soportar los cambios sin degradar la calidad del software que producimos.
 Para testear que el método onClick() funciona necesito hacer pruebas de integración que abarquen tanto interfaz de usuario como objetos DAO/Home y de negocio. Al tener separado objetos que dependen de la tecnología de presentación y objetos que no, podemos hacer testing automatizado y unitario sobre el buscador, sobre los objetos de dominio e incluso sobre los repositorios (a través de objetos mock/impostores si es necesario).

Resumen

  • Vista de edición: que sigue el esquema MVC: el modelo de la vista es un objeto de negocio, aunque podríamos trabajar también con un application model
  • Vista de búsqueda: sigue el esquema MMVC: el modelo de la vista es un objeto de aplicación, intermediario entre la vista, el controller y el objeto de negocio

Importante: para el TP

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

ą
Fernando Dodino,
19 jul. 2011 14:00
ą
Fernando Dodino,
17 ago. 2011 6:12
ą
Fernando Dodino,
17 ago. 2011 6:16