miércoles, 30 de septiembre de 2009

Flex/AIR: Eventos personalizados

La dinámica de desarrollo en Flex está orientada a crear componentes independientes lo más atomizado posible, con el fin de componer interfaces ricas de forma ergonómica e independiente, lo que además, permite aislar problemas concretos dentro de las interfaces, además de contribuir a la reutilización de los mismos.

Para entender un poco mejor el párrafo anterior imaginemos una interfaz típica en la que hay un grid con el conjunto de datos, y además una ficha de registro. Podemos pensar en realizar todo en un mismo contenedor, ya sea en la misma visualización, activando desactivando opciones según se requieran, o bien crear estados para activar o desactivar el modo lista o el modo ficha.

Para ir un poco más finos, se puede crear un componente lista que se encargue de forma exclusiva de presentar los datos en un DataGrid, y operar con unos filtros para búsquedas más concretas de información.

Para ser lo más interactivos posibles, habría un botón "Agregar" para añadir un nuevo registro, con lo que presentaría la ficha vacía. Para editar o eliminar un dato concreto, cuando el usuario haga clic sobre una fila determinada se mostraría la ficha con los datos.

El componente ficha aparecería cuando se pulse el botón "Agregar" en modo edición y vacío. Si se hace clic sobre una fila, aparecería en modo visualización, con la opción de editarlo o eliminarlo. Como acciones tendría "Guardar", "Eliminar", "Cancelar" y "Cerrar". Su objetivo es tratar individualmente los datos de un registro específico.

El contenedor padre únicamente insertaría estos componentes y realizar las relaciones entre ambos según se requieran, pero los objetivos concretos están dentro de cada componente, y en el caso de depurar, corregir, modificar o agregar funcionalidades, el nivel de aislamiento permite claramente "tocar" sólo la parte responsable.

Espero que con este planteamiento esté claro la organización de una funcionalidad clásica de tratamiento y gestión de datos.

Ahora bien, un pensamiento típico para aquel que empieza a desarrollar con componentes es crear las relaciones con elementos (propiedades, variables o métodos) públicos). Esto, además de complicar el desarrollo, la sincronización entre las partes estaría forzada de forma implícita por código, lo que haría un sistema un tanto inestable e inseguro, amén de sacrificado por el esfuerzo que requiere después modificaciones o correcciones. A esto se le denomina acoplamiento fuerte, y todo depende de otras acciones u operaciones.

Es posible hacer más sencillo este desarrollo realizando un acoplamiento débil, y que no sea todo tan dependiente y preocuparnos de las relaciones cuando deban ocurrir, y de una forma menos forzosa. Para ello, podremos definir nuestros propios eventos en los componentes, y a través de ellos pasar información (como los datos de una fila o la acción a emprender).

Para empezar, recomiendo utilizar dos clases en ActionScript. La primera de ellas será una clase para almacenar la información que se pasará al evento, como los datos de la fila seleccionada o la acción solicitada por el usuario. Puede declararse en la carpeta o paquete que uno requiera.

package com.agenda
{
  public class Agenda
  {
    public static const ACTION_NEW:int=0;
    public static const ACTION_SELECT:int=1;
    public var accion:int;
    public var nombre:String;
    public var direccion:String;
    public var telefono:String;

    public function Agenda()
    {
      accion=ACTION_NEW;
      nombre="";
      direccion="";
      telefono="";
    }

    public function toString():String
    {
      return "accion:"+accion+"|"+
        nombre+"|"+
        direccion + "|" +
        telefono;
    }

  }
}

Básicamente define los campos, un constructor por defecto (con una carga inicial de datos) y un método toString() que muestra la información de la instancia actual.

La segunda clase define el evento personalizado, y para ello se crea una clase que hereda de la superclase base Event y, al igual que la otra clase, podemos colgarla del paquete que creamos más oportuno (en este caso en el mismo paquete que la anterior):

package com.agenda
{
  import flash.events.Event;

    public class AgendaEvent extends Event
    {
      public var agenda:Agenda;

      public function AgendaEvent(agenda:Agenda, type:String)
      {
        super(type);
        this.agenda=agenda;
      }

      public override function clone():Event {
        return new AgendaEvent(agenda, type);
      }
    }
}

La clase Event es la clase básica para cualquier evento, y por ello, esta clase hereda (extiende) aquella, añadiendo una funcionalidad propia. En primer lugar se crea una propiedad que contiene un objeto de tipo Agenda (definido en la clase anterior), conteniendo la información necesaria. A continuación se crea un constructor especial, en donde se pasa el objeto Agenda a tratar y el tipo de evento a construir (que será de este tipo). El constructor invoca a su superclase indicando este tipo, y asigna la información al objeto Agenda.

Se sobreescribe el método clone(), el cual crea y devuelve un objeto evento de sí mismo (mejor no entremos en detalle, pero es un punto importante a implementar).

El siguiente paso será ir al componente que va a generar el evento, en nuestro caso al componente del DataGrid, que es el que contiene la lista de datos. En este componente hay que declarar el evento para que sea visible por el resto de componentes que lo utilicen. Para ello, hay que crear este código justo después del comienzo de la declaración del componente (el contenedor que lo forma. Canvas, HBox, VBox...), que sea el primer código del mismo (no es esencialmente así, pero sí recomendable):

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas
xmlns:mx="http://www.adobe.com/2006/mxml"
width="450"
height="400"
>

<!-- EVENTS -->
<mx:Metadata>
[Event(name="selectAgenda",type="com.agenda.AgendaEvent")]
[Event(name="addAgenda",type="com.agenda.AgendaEvent")]
</mx:Metadata>

  ...

</Canvas>

El bloque Metadata declara dos eventos para este componente:
- selectAgenda -> cuando el usuario hace clic sobre una fila del DataGrid
- addAgenda -> cuando el usuario hace clic sobre el botón "Agregar"

Ambos eventos son del tipo AgendaEvent.

Esta parte sólo declara los eventos, para que sea visible por el componente padre que utiliza a éste (se puede probar a insertar el componente y con Ctrl+Espacio extraer las propiedades y métodos de este componente, donde aparecerán estos dos eventos). En realidad la declaración no hace que se produzcan, pues hay que controlar cuándo y cómo se lanzan estos dos eventos.

El primero de ellos se lanza cuando el usuario hace clic sobre una fila del DataGrid. Para ello, se captura el evento de la selección:

<mx:DataGrid id="dgAgenda"
  itemClick="selectItemAgenda();"
  ...

El código correspondiente para despachar el evento "selectAgenda" es el siguiente:

private function selectItemAgenda():void
{
  var miAgenda:Agenda = new Agenda();
  miAgenda.action = Agenda.ACTION_SELECT;
  miAgenda.nombre = dgAgenda.selectedItem.nombre;
  miAgenda.direccion = dgAgenda.selectedItem.direccion;
  miAgenda.telefono = dgAgenda.selectedItem.telefono;
  var e:AgendaEvent = new AgendaEvent(agenda, "selectAgenda");
  this.dispatchEvent(e);
}

Se instancia la clase Agenda para almacenar la información que se va a utilizar en el componente principal o padre. La información se extrae del DataGrid (dgAgenda), de la fila actualmente seleccionada (selectedItem) y de cada uno de los campos definidos en el DataGrid (nombre, direccion y telefono).

A continuación se crea un objeto de tipo evento AgendaEvent, pasando esta información, y dando el nombre del evento "selectAgenda" que ya fue declarado (bloque MetaData). Por último, se despacha el evento (dispatchEvent), que saltará en el componente padre cuando éste se produzca.

Para el evento addAgenda el código es similar. Al hacer clic sobre el botón se invoca al método que despachará el evento:

<mx:Button id="btnAdd" click="addItemAgenda();" />

El código a implementar sería el siguiente:

private function addItemAgenda():void
{
  var agenda:Agenda = new Agenda();
  var e:AgendaEvent = new AgendaEvent(agenda, "addAgenda");
  this.dispatchEvent(e);
}

Cuando el usuario haga clic sobre el botón "Agregar" se creará el evento "addAgenda", que será lanzado hacia los componentes padre que utilicen este componente.

Ahora queda la parte en que estos eventos son capturados por el componente padre o que contiene a este componente. He de reconocer que me volví un poco loco porque creí que no me funcionaba todo lo anterior, ya que al incrustar el componente del DataGrid dentro del componente principal, al intentar ver los eventos, métodos y propiedaes con Ctrl+Espacio en el editor de Flex, no me aparecía nada. Incluso traté de declarar el evento para lanzar un método simple o lanzar un Alert, pero no funcionaba. Si esto os ocurre os contaré por qué.

Utilizo un paquete específico para los componentes. Si se ubica un componente en un paquete o carpeta distinto al del componente padre, parece no verlo, aunque se especifique la ruta completa de forma implícita y se vea el componente y aparentemente funciona (recoge datos y los visualiza, e incluso funciona el filtro). Pero los eventos no funcionaban. Al final, he ubicado todos los componentes, tanto padres e hijos en el mismo paquete, y así iba bien. Estoy de acuerdo que esta explicación no puede ser del todo convincente, pero el hecho es que así me funcionó a mi.

Ahora, en el componente padre, cuando se incrusta el componente hijo, al dar el espacio y al pulsar las teclas Ctrl+Espacio, se verán los eventos declarados y dispuestos para ser utilizados:

<components:ListaAgenda
  selectBatch="doSelectAgenda(event);"
  addBatch="doAddAgenda(event);"
/>


Al enviar el parámetro "event", éste contendrá la información contenida:

private function doSelectAgenda(e:AgendaEvent):void {
  Alert.show("Evento selectAgenda: " + e.agenda.toString());
}

private function doAddAgenda(e:AgendaEvent):void {
  Alert.show("Evento addAgenda: " + e.agenda.toString());
}

Espero que este tutorial básico sobre eventos personalizados os sea de utilidad.