Mostrando entradas con la etiqueta AIR. Mostrar todas las entradas
Mostrando entradas con la etiqueta AIR. Mostrar todas las entradas

miércoles, 3 de noviembre de 2010

Flex/AIR: DataGrids dinámicos (2)

En el anterior post sobre Datagrids dinámicos (http://rafinguer.blogspot.com/2010/11/flexair-datagrids-dinamicos.html), expusimos un ejemplo práctico en base a un XML clásico y básico. El planteamiento expuesto contempla que el nombre de cada elemento es también el texto de la columna. Pero, ¿y si el texto de la columna debe contener caracteres no alfabéticos, espacios, signos, etc.? Entonces no nos vale. Hemos de replantear el XML, por ejemplo, de la siguiente manera:

<?xml version="1.0" encoding="utf-8"?>
<vista2d>
 <fila>
  <columna nombre="Costes financieros">99898.95</columna>
  <columna nombre="Costes globales">78700.87</columna>
  <columna nombre="Costes internos">54980.76</columna>
 </fila>
 ...
</vista2d>


Las columnas serán todas elementos columna, y el nombre de la columna vendrá dado por el atributo nombre. Por tanto, ha de recogerse el atributo, no el nombre del elemento. Esto plantea un problema, pues a la hora de crear la columna (mediante DataGridColumn), hay que asignar el argumento como texto de cabecera (propiedad headerText), pero la creación de la misma requiere de un nombre. Al ser dinámico, también ha de tener un nombre único. Esto se podría realizar mediante el siguiente código:

// Extraccion de columnas
var cols:Array = new Array(); // Crear array de columnas
var iCol:int=0;

for each(var element:XML in vxmll[0].elements()) {
  var col:DataGridColumn = new DataGridColumn("c"+iCol);
  col.headerText=element.attribute("nombre");
  cols.push(col);
  iCol++;
}

dgVista2D.columns = cols; // Asignar columnas al DataGrid
dgVista2D.validateNow(); // Validar y refrescar el DataGrid


Como se puede comprobar, hay un contador de columnas (de 0 a n), el cual servirá para asignar el nombre a la columna ("c1", "c2", "c3", etc.)

El problema que viene a continuación es que si se convierte los datos del XML almacenado en vxmll a un ArrayCollection, y asignárselo al DataGrid, no aparecerá ningún dato. ¿El motivo? El nombre de las columnas.

La solución está en crear un ArrayCollection que contenga un objeto por cada fila, que contenga tantas propiedades como columnas tenga, y que dichas propiedades se llamen igual que las columnas. Como todo es dinámico, habrá que crear objetos en tiempo de ejecución (ver anterior post: http://rafinguer.blogspot.com/2010/11/flexair-creacion-de-objetos-en-tiempo.html).

El siguiente código realizará la conversión correcta para que el DataGrid muestre correctamente los datos:

// Procesamiento de los datos
acResult = new ArrayCollection();

// Recorre las filas
for each(var elem:XML in vxmll) {
  // Recorre las columnas
  iCol = 0;
  var o:Object = new Object();

  for each(var subelement:XML in elem.elements()) {
    o["c"+iCol]=subelement.toString();
    iCol++;
  }
  acResult.addItem(o);
}

dgVista2D.dataProvider=acResult;


El código completo para la función srvObtenerVista2DResult() es el siguiente:

private function srvObtenerVista2DResult(event:ResultEvent):void {
  var vxmll:XMLList = new XMLList(event.result.fila);
  
  // Si hay datos
  if (vxmll.length()>0) {
    // Extraccion de columnas
    var cols:Array = new Array(); // Crear array de columnas
    var iCol:int=0;
    
    for each(var element:XML in vxmll[0].elements()) {
      var col:DataGridColumn = new DataGridColumn("c"+iCol);
      col.headerText=element.attribute("nombre");
      cols.push(col);
      iCol++;
    }
    
    dgVista2D.columns = cols; // Asignar columnas al DataGrid
    dgVista2D.validateNow(); // Validar y refrescar el DataGrid
    
    // Procesamiento de los datos
    acResult = new ArrayCollection();
    
    // Recorre las filas
    for each(var elem:XML in vxmll) {
      // Recorre las columnas
      iCol = 0;
      var o:Object = new Object();
      
      for each(var subelement:XML in elem.elements()) {
        o["c"+iCol]=subelement.toString();
        iCol++;
      }
      acResult.addItem(o);
    }
    
    dgVista2D.dataProvider=acResult;
  }
}

Flex/AIR: Creación de objetos en tiempo de ejecución

Dentro de las posibilidades que nos ofrece Flex, una de las más potentes y útiles es la de crear objetos en tiempo de ejecución. Hay muchos casos de los que se podría hablar, pero en este post voy a comentar uno que me ha surgido, y que seguramente os pueda surgir a vosotros también.

El caso del que voy a hablar trata de cuando se obtienen datos de alguna fuente, de forma dinámica, es decir, que pueden llegar con nombres de campo y con valores variopintos y distintos. Casos como éste puede ocurrir cuando se quiere crear un mapeo automático de tablas o de consultas a bases de datos, de un XML impredecible, de un JSON, etc. En este escenario, de nada nos sirve predecir una clase con sus propiedades.

Para crear un objeto que contenga estos datos dinámicos, primeramente hay que definir una variable de tipo Object:

var o:Object = new Object();

Para crear una propiedad dinámica con su respectivo valor, la declaración sería de la siguiente manera:

o["propiedad"] = valor;

Para acceder al valor de la propiedad:

o["propiedad"]

Cada vez que se agrega una propiedad al objeto, ésta es accesible mediante una enumeración. Para recorrer las propiedades definidas en el objeto:

for (var detalle:* in o) {
  ...
}


La variable detalle contiene el nombre de la propiedad. Gracias a este valor se puede acceder a la propiedad y, por tanto, también a su valor:

for (var detalle:* in o) {
  Alert.show(lbCaracteristicas.text +
    "-["+detalle+"]: "+o[detalle];
}


A continuación, un código de ejemplo para experimentar con todo lo aportado en este post:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
 xmlns:s="library://ns.adobe.com/flex/spark"
 xmlns:mx="library://ns.adobe.com/flex/mx"
 creationComplete="init()">

 <mx:HBox>
  <s:Label id="lbPropiedad" fontWeight="bold"/>
  <s:Label id="lbValor" fontWeight="normal"/>
  <s:Label id="lbCaracteristicas" fontStyle="italic"/>
 </mx:HBox>

 <fx:Script>
  <![CDATA[
   private function init():void {
    var propiedad1:String = "nombre";
    var valor1:String = "Rafael Hernamperez";
    var propiedad2:String = "edad";
    var valor2:int = 40;
    var info:Object = new Object();
    info[propiedad1] = valor1;
    info[propiedad2] = valor2;
    lbPropiedad.text = propiedad1;
    lbValor.text = info[propiedad1];

    for (var detalle:* in info) {
     lbCaracteristicas.text = lbCaracteristicas.text +
      "-["+detalle+"]: "+info[detalle];
    }
   }
  ]]>
 </fx:Script>
</s:Application>


El resultado debería ser el siguiente:

nombre  Rafael Hernamperez  -[nombre]: Rafael Hernamperez-[edad]: 40

Flex/AIR: DataGrids dinámicos

Voy a plantear un problema que me ha surgido recientemente. En una aplicación se ha de mostrar la información recogida de una consulta lanzada por HTTPService, y debe ser plasmada en un DataGrid. En principio es simple, pero, ¿y si el resultado de la consulta genera resultados diferentes en el XML, tales como las columnas?. Imaginemos que en una consulta nos retorna el siguiente XML:

<?xml version="1.0" encoding="utf-8"?>
<vista2d>
 <fila>
  <Servicios>...</Servicios>
  <Costes>...</Costes>
  <Ventas>...</Ventas>
 </fila>
 ...
</vista2d>


En otra consulta nos retorna este otro XML:

<?xml version="1.0" encoding="utf-8"?>
<vista2d>
 <fila>
  <Objetivos>...</Objetivos>
  <Ventas>...</Ventas>
  <Gastos>...</Gastos>
  <ROI>...</ROI>
  <Desviacion>...</Desviacion>
 </fila>
 ...
</vista2d>


En un DataGrid hemos de especificar implícitamente cada una de sus columnas para que éstas se visualicen. Al menos, cuando utilizamos MXML. Para poder generar DataGrids dinámicos, trabajaremos con ActionScript.

En primer lugar, tendremos definido el DataGrid, el cual, al no conocer qué columnas va a tener, éstas no serán declaradas. Por tanto, el DataGrid no contendrá columnas:

<mx:DataGrid id="dgVista2D" width="100%" height="100%"/>

A continuación declararemos el servicio HTTPService, el cual será llamado mediante el método send(). El código sería similar al siguiente:

 <s:HTTPService id="srvObtenerVista2D"
   resultFormat="e4x"
   method="GET" useProxy="false"
   url="http://localhost:8080/ticube/SrvObtenerVista2D"
   fault="getHTTPServiceFault(event)"
   result="srvObtenerVista2DResult(event)"/>


Hemos de importar las siguientes clases para poder lanzar eventos en caso de que la llamada al HTTPService produzca algún error (fault), o en el caso de que se haya procesado con éxito (result):

import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;


En cada uno de estos casos, al producirse el evento, se ejecutará el método asociado. En el caso de que la llamada produjera un error (como en el caso de que no acceda a la URL, el servicio está caído, etc.), se lanzará un mensaje de error:

// Error en carga de datos HTTP
private function getHTTPServiceFault(event:FaultEvent):void {
  Alert.show("Error: " + event.toString());
}


En el caso de que la llamada se efectuara correctamente, y retornara el XML correspondiente, éste se pasará como parámetro (event.result). El código completo es el siguiente (a continuación se explicará en detalle):

private function srvObtenerVista2DResult(event:ResultEvent):void {
  var vxmll:XMLList = new XMLList(event.result.fila);

  // Extraccion de columnas
  if (vxmll.length()>0) {
    var cols:Array = new Array(); // Crear array de columnas
    for each(var element:XML in vxmll[0].elements()) {
      // element.name retorna el titulo del elemento
      // element.toString retorna el valor del elemento
      cols.push(new DataGridColumn(element.name())); // Añadir columna al array
    }
  }

  dgVista2D.columns = cols; // Asignar columnas al DataGrid
  dgVista2D.validateNow(); // Validar y refrescar el DataGrid

  // Convertir el XML retornado a ArrayCollection
  // y cargar los datos en el DataGrid
  var vxmllc:XMLListCollection = new XMLListCollection(vxmll);
  var aResult:Array = vxmllc.toArray();
  var acResult:ArrayCollection = acResult = new ArrayCollection(aResult);
  dgVista2D.dataProvider=acResult;
}


El parámetro event recoge toda la información del evento, incluyendo el resultado (result), el cual será el XML retornado por el HTTPService.

private function srvObtenerVista2DResult(event:ResultEvent):void {

Cuando se accede al miembro fila de este XML, éste retornará una lista (XMLList) de elementos fila, cada uno de los cuales es un XML que contiene los elementos de nivel inferior (columnas y valores por columna).

  var vxmll:XMLList = new XMLList(event.result.fila);

Si esta lista posee información (el XML no vino vacío), se creará un array (cols) que almacenará los objetos de columna (objetos DataGridColumn) que se añadirán al DataGrid). Se recorren los elementos de la primera fila (primer elemento de la lista), creando un objeto de columna, cuyo texto será el nombre del elemento substraído, y el cual será añadido al array.

  // Extraccion de columnas
  if (vxmll.length()>0) {
    var cols:Array = new Array(); // Crear array de columnas
    for each(var element:XML in vxmll[0].elements()) {
      // element.name retorna el titulo del elemento
      // element.toString retorna el valor del elemento
      cols.push(new DataGridColumn(element.name())); // Añadir columna al array
    }
  }

Finalizado el bucle, el array cols tendrá todas las columnnas (objetos DataGridColumn), con el texto de cada elemento leído de la primera fila del XML. Ahora sólo queda asignar dicho array al DataGrid, y a continuación validar dicho DataGrid para que se actualice y visualice correctamente.

  dgVista2D.columns = cols; // Asignar columnas al DataGrid
  dgVista2D.validateNow(); // Validar y refrescar el DataGrid


Por último, hay que asignar la información (datos) al DataGrid. Para ello, se transforman los datos de la lista (XMLList) a una colección (XMLListCollection), de ésta a un array, y de éste a un objeto ArrayCollection, el cual se puede asignar al DataGrid, mediante su propiedad dataProvider:

  // Convertir el XML retornado a ArrayCollection
  // y cargar los datos en el DataGrid
  var vxmllc:XMLListCollection = new XMLListCollection(vxmll);
  var aResult:Array = vxmllc.toArray();
  var acResult:ArrayCollection = acResult = new ArrayCollection(aResult);
  dgVista2D.dataProvider=acResult;

martes, 20 de octubre de 2009

Flex/AIR: Error de conversión a ArrayCollection de ObjectProxy en llamadas HTTPService

Me he encontrado con un problema habitual, y ocurre cuando uno hace una llamada a un HTTPService, y en la función que gestiona (handle) la llamada, al asignar el resultado del XML a un ArrayCollection, se encuentra con que no puede asignarse un objeto ObjectProxy al ArrayCollection. Este tipo de operaciones son habituales cuando hay que cargar una lista o un ComboBox. Este problema ocurre cuando solamente se tiene un elemento en la lista, mientras que si hay más de uno, este problema no ocurre. Asimismo, si no hay resultado, también puede dar un error de null. Creo que se trata de un bug de Flex, ya que en estos casos, automáticamente debería dar un ArrayCollection de todas maneras, con cero, uno o más de un item.

Antes de continuar, indicar que este post no contiene un ejemplo completo para poder probar, si no que explica detalladamente como resolver esta situación.

Tras dar vueltas y vueltas al problema, he realizado una pequeña "ñapa" o "parche" para que la carga del ArrayCollection se realice sin problemas.

En primer lugar voy a presentar el XML que devuelve el HTTPService:




<?xml version="1.0" encoding="ISO-8859-1" ?>

<products_providers_in_use>
  <product_provider_in_use>
    <data>6</data>
    <label>Bill Gates</label>
    <id_product>21</id_product>
    <name_product>Escalope de ternera</name_product>
    <total>0</total>
  </product_provider_in_use>
  ...
<products_providers_in_use>




La definición del HTTPService permite definir en qué funciones delegar la ejecución en caso de error (fault) o de éxito (result), así como la URL al HTTPService (url).



<!-- Call to srvGetProductsProvidersInUse service -->
<mx:HTTPService 
  result="handleProductsProvidersInUse(event);"
  fault="handleFault(event);" 
  id="nombre_del_servicio" resultFormat="object"
  url="url_al_httpservice"
  useProxy="false">
</mx:HTTPService>



Es necesario definir el ArrayCollection como una variable de ámbito local, que pueda ser accedida por todas funciones del módulo, componente, etc.



[Bindable]
import mx.collections.ArrayCollection;

private var acInUse:ArrayCollection;



La llamada al HTTPService se realiza mediante el objeto HTTPService (referenciando el nombre indicado en la propiedad id) y al método "send" en el momento que sea necesario (en el creationComplete, al pulsar un botón, etc.):



nombre_del_servicio.send();



Una vez se ejecuta el HTTPService, éste accederá a dicho servicio para obtener un XML. En el caso de que no se haya producido ningún problema en la comunicación, se delegará la ejecución en la función definida en "result". El código para leer correctamente este XML en todos los casos (ya corregido el bug) es el siguiente:



private function handleProductsProvidersInUse(event:ResultEvent):void {
  try {
    acInUse = 
      event.result.products_providers_in_use.product_provider_in_use
        as ArrayCollection;
  }
  catch (err:Error) {
    acInUse=new ArrayCollection();
  }
  if (acInUse==null) {
    var o:Object;
    o=event.result.products_providers_in_use.product_provider_in_use;
    acInUse = new ArrayCollection(
      [{data:o.data,label:o.label,id_product:o.id_product,
      name_product:o.name_product,total:o.total}]);
  }
}



Para controlar las excepciones se abre un bloque try...catch para intentar acceder al XML recogido. Lo normal es asignar al ArrayCollection la colección de datos del XML que se repite. El XML está contenido dentro del parámetro de evento, y en la propiedad "result". El primer elemento del XML ("products_providers_in_use") no se repite, y marca el principio y fin del grupo de datos. Por ello hay que llegar hasta el siguiente nivel del XML: "product_provider_in_use". En teoría, este nivel retornaría un ArrayCollection. En el caso de que tenga más de un elemento no hay problema, pero si hay un sólo elemento, no produce ninguna excepción, pero, inexplicablemente, nos retorna un null. Por ello, tras el catch se hace una comprobación de si es nulo, en cuyo caso se crea un ArrayCollection nuevo, se recoge el único elemento del resultado en un objeto genérico, y se crea un elemento con la información recogida en el XML. En el caso de que el XML estuviera vacío o tuviera algún problema, saltaría una excepción, la cual es recogida por el bloque "catch" y crea un ArrayCollection nuevo, pero vacío, pero no null.

Espero que este ejemplo corte definitivamente el tiempo que os haya hecho perder esta singular situación.

Safe Creative #1001195348495

Flex/AIR: Listas personalizadas con ItemRenderer

Una de las mayores virtudes de Flex es que es posible personalizar la vista de una lista o de un Datagrid, pudiendo añadir elementos que no son estándares (la típica etiqueta o texto), sino que se pueden añadir también elementos gráficos o incluso elementos de interfaz de usuario.

Para ver una muestra de ello, la siguiente imagen muestra una lista con un icono a la izquierda, dependiendo del estado del item:

Como puede apreciarse, aunque sea un ejemplo sencillo, da alas a multitud de posibilidades, tanto de visualización como de edición.

El poder de ItemRenderer es poder incrustar en lugar de un item un componente. Esto es, en lugar de una fila en una lista, o de una columna en un DataGrid.


Planteamiento
El planteamiento de este componente o, mejor dicho, su utilidad, es poder realizar una lista que nos permita seleccionar un elemento, pero que, dependiendo de su estado, se pueda interactuar de una manera u otra.

Por ejemplo, imaginemos una lista de frutas asociadas a un pedido. Puede haber frutas que han sido ya entregadas (estado false o no disponibles) y otras que aún no han sido entregadas (estado true o disponibles). De esa lista, es posible quitar frutas para que no sean servidas. Obviamente, aquellas que ya han sido entregadas no podrán quitarse del pedido, y sí aquellas aún no han sido entregadas. Además del dato de estado, una ayuda visual se agradece, indicando con un pequeño icono o algún estilo de texto cuáles están disponibles y cuáles no.


Componente de contenido

El componente de contenido será aquel que será renderizado en el item. En nuestro caso será un simple HBox, que contendrá una imagen de 20x20 (en formato png, por las transparencias), y una etiqueta con el texto a visualizar.

El código de este componente es el siguiente:


Es importante señalar que en una lista la interacción de los datos ha de coordinarse, pues al hacer clic sobre el item, éste también debe retornar la información. Si no fuera así, sólamente sería un elemento decorativo y no se podría seleccionar. Es por ello que se haya sobreescrito (override) los método set data y get data. Cuando la lista comienza a dibujar (render) cada item, utiliza este componente que hemos utilizado, y le pasa (set) los datos. Internamente, cuando recibe los datos, llama al método refresh para pintarlos adecuadamente. Por otro lado, cuando el usuario hace clic sobre el item, éste debe devolverle (get) a la lista los datos para que pueda seleccionarse e interactuar con el código.

Otra cosa importante a señalar es el uso de la función loadImage:


// This function loads the image
private function loadImage(imgURL:String):void {
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
function (e:Event):void {
itImage.source=e.currentTarget.content; });

loader.load(new URLRequest(encodeURI(imgURL)));
}


Se le llama con la ruta donde se alojan las imágenes (puede ser en disco duro, o una URL externa, o una referencia desde el directorio raíz). En el ejemplo, se ha creado un directorio "resources" dentro del raíz del código Flex, y por ello se indica en la ruta como "resources/imagen". Esta función es más recomendable que usar simplemente la propiedad source de la imagen, pues además permite recoger la información correspondiente a la imagen si queremos hacer uso de ella.

LISTA

En el ejemplo, se visualizará la lista correspondiente a partir de unos datos almacenados en un ArrayCollection. Estos datos están incrustados en el código, pero podrían ser leídos de un HTTPService o de un servicio Web.

Los campos de información son los siguientes:
- label: etiqueta o texto a visualizar en la lista
- data: código numérico del producto (no se visualiza)
- state: true/false. Indica la disponibilidad del item

El código es el siguiente:

El punto de inflexión está en la propiedad itemRenderer de la lista, en donde se indica qué componente va a dibujarse en el item de la lista.

El resultado será, además del visual, el que cuando un usuario seleccione un item de la lista, se mostrará una alerta con los datos del item seleccionado, tal y como muestra la siguiente imagen:


Safe Creative #1001195347856

domingo, 18 de octubre de 2009

Flex/AIR: Teclado virtual avanzado para pantallas táctiles

Ya está disponible una nueva versión del teclado virtual para pantallas táctiles, con las siguientes mejoras y cambios:

- Posibilidad de elegir entre un texto simple (una línea) o un texto multilínea.
- Se ha evitado los estados para hacer más sencilla la recepción del texto, con tan sólo cerrar la ventana.
- La tecla "Aceptar" se ha sustituito por el "Intro" o retorno de carro.
- Se ha agregado el botón "Anular", con el que se anula por completo el texto actual.
- Se puede interactuar con el teclado tradicional, haciendo editable el display. De esta manera, cuando el texto sea más grande que el display, se hará scrolling, accediendo a la zona de edición (en la versión anterior se cortaba, pues sólo se veía el principio).
- Es posible editar texto para su sustitución por un carácter pulsado en el teclado, o por la tecla borrar.
- Es posible especificar el tamaño máximo del texto (número de caracteres).
- Se ha incluido un status en el que se visualiza cuántos caracteres hay escritos y cuántos caracteres se pueden escribir (tamaño máximo del texto).



En la segunda demo, se ha incluído un fondo para ver cómo el teclado utiliza las transparencias con el fondo. Hay una caja de texto simple y una TextArea. La edición de ambas cajas de texto se realiza, simplemente, haciendo click sobre cada una de ellas.

Cajas de texto simple y multilínea


Teclado virtual avanzado editando multilínea


El código de la demo es el siguiente:


La explicación del código es muy similar a la del ejemplo anterior (mirar http://rafinguer.blogspot.com/2009/10/flexair-teclado-virtual-para-pantallas.html para el detalle). Tan sólo añadir que se pasa al componente, a través de la función "setTextType()", el tipo de texto a visualizar: TYPE_SINGLE (opcional) o TYPE_MULTILINE (en caso de querer un texto multilínea). Asimismo, también se especifica el tamaño máximo a editar para el texto, mediante la función "setMaxChars()".

Otro detalle a considerar es que ahora, al no tener estados o acciones (sólo el cierre de la ventana del teclado), el evento a gestionar es de tipo "Event.CLOSE", cuya función delegada simplemente ha de obtener el texto (función "getText()") sin necesidad de evaluar estados.

Para ver la demo en directo: http://www.tecnillusions.com/demos/DemoKeyboard2/DemoKeyboard2.html

Para descargarse el código fuente y el ejecutable: http://www.tecnillusions.com/demos/DemoKeyboard2/DemoKeyboard2.zip

sábado, 17 de octubre de 2009

Flex/AIR: Teclado virtual para pantallas táctiles

En esta ocasión no voy a explicar algún detalle sobre alguna técnica concreta, si no que voy a publicar, íntegramente un componente que muestra un teclado virtual que puede ser utilizado en lugar del teclado físico, ya sea con el ratón o bien en una pantalla táctil.

El teclado es de tipo QWERTY, y tiene tres modos: normal (minúsculas), mayúsculas y símbolos. Incluye también los números.

Otro detalle importante es que posee transparencia con el fondo y posee efectos especiales al utilizar los botones. ¿Qué más se puede pedir?

El aspecto que presenta es el siguiente:

Modo normal


Modo Mayúsculas


Modo Símbolo


El modo de utilizarlo es muy sencillo. El código para el ejemplo es el siguiente:



El ejemplo parte de que el componente está en la misma carpeta que la aplicación. Si no fuese así, habría que hacer el "import" correspondiente.

El ejemplo es muy sencillo: en una caja de texto escribiremos parte de un texto. Cuando se pulse el botón "Teclado" aparecerá el el teclado virtual con el texto escrito. En ese momento se modificará el texto con este teclado. Al darle al botón "Aceptar", el nuevo texto aparecerá en la caja de texto. Si se cerrase la ventana con el botón de cierre, mostrará un texto indicando que el texto no fue aceptado, y no habrá cambio alguno.

El componente KeyboardWindow es un "TitleWindow" o PopUp. Hay que importar la clase PopUpManager y declarar una variable de este tipo KeyboardWindow:


import mx.managers.PopUpManager;
import mx.controls.Alert;

private var wKeyboard:KeyboardWindow;



Cuando el usuario haga click sobre el botón "Aceptar" se ejecutará la función "showKeyboard()":


private function showKeyboard():void {
  wKeyboard = KeyboardWindow(PopUpManager.createPopUp(
    this, KeyboardWindow, true));

  wKeyboard.addEventListener(Event.REMOVED, checkText);
  PopUpManager.centerPopUp(wKeyboard);
  wKeyboard.setText(myText.text);
}


La primera sentencia crea la ventana emergente (PopUp), indicando cual es el contenedor padre (this o la propia aplicación), así como de qué tipo es la ventana hija (KeyboardWindow).

La segunda sentencia agrega el evento Event.REMOVED que permite escuchar cuándo la ventana emergente se cierra, enviando la ejecución del código a la función checkText.

La tercer sentencia se encarga de centrar la ventana emergente dentro del contenedor (en nuestro caso, la aplicación).

La cuarta sentencia le pasa al teclado virtual el texto que tenemos en la caja de texto, para su gestión.

Por último, queda la función checkText(), cuyo código es el siguiente:


private function checkText(e:Event):void {
  if (wKeyboard.action==wKeyboard.ACTION_OK)
    myText.text = wKeyboard.getText();
  else if (wKeyboard.action==wKeyboard.ACTION_CANCEL)
    Alert.show("Texto no aceptado");
}


El componente KeyboardWindow tiene una variable que contiene el estado o acción realizada antes de cerrar la ventana. Si se hizo click sobre el botón "Aceptar", el estado será ACTION_OK. Si se hizo click sobre el botón de cierre de la ventana, la acción o estado será ACTION_CANCEL. En el primer caso, se recogerá el texto gestionado por el teclado virtual mediante el método getText(), y se colocará en la caja de texto principal. En el segundo caso, se mostrará una alerta indicando que el texto no fue aceptado.

El aspecto final de este ejemplo es el siguiente:



Para ejecutar el ejemplo en directo:
http://www.tecnillusions.com/demos/DemoKeyboard/DemoKeyboard.html

Para descargar el componente y el código fuente:
http://www.tecnillusions.com/demos/DemoKeyboard/DemoKeyboard.zip

martes, 13 de octubre de 2009

Flex/AIR: Reemplazar datos en tiempo de visualización

Hay ocasiones en las que ha de tratarse los datos que se van a visualizar. Por ejemplo, imaginemos una tabla en la que se visualiza los datos recogidos de una consulta, pero el valor de cierto campo posee datos que puedan ser ambiguos, tales como un número (códigos, por ejemplo) o valores true/false. Los datos son almacenados así en el origen de datos, pero mostrarlos al usuario puede ocasionarle confusión. En estos casos sería de gran utilidad poder definir qué valores se van a visualizar (como el texto correspondiente a cada código o un texto en lugar de true o false).

Aunque este post vaya dirigido al DataGrid y el AdvancedDataGrid, sirve también para otros componentes que muestren la información basada en un ArrayCollection, tales como una lista, un combo o un Tree.

Flex/AIR nos permite controlar el modo en que se van visualizando los datos a partir de la colección de datos. Esto se puede controlar gracias a la propiedad labelFunction, que estos componentes.

El funcionamiento es muy sencillo. Se indica en esta propiedad el nombre de una función o método que se ejecutará cada vez que trate el dato (en cada fila y columna en el caso de los DataGrid). En la definición del correspondiente DataGridColumn o de AdvancedDataGridColumn (el que contenga la revisión de la información a tratar). Ejemplo:


<mx:AdvancedDataGridColumn
  dataField="product_raw"
  labelFunction="replaceValuesColumn"/>


La función se definiría así:


private function replaceValuesColumn(
  item:Object, column:AdvancedDataGridColumn):String
{
  // Código que tratará cada dato
}


Los parámetros dependerán de cada tipo de componente. En el caso de un DataGrid, el parámetro 'column' será de tipo DataGridColumn. Las listas no tienen este parámetro.

Esta función se lanzará cada vez que el DataGrid (en este caso) vaya a rellenar esa columna en la fila actual. Aquí se evaluará los valores y, en consecuencia, decidir qué dato visualizar, el cual será indicado en el return de la función en formato String. En nuestro caso, el campo' product_raw' contiene los datos true o false, que indican si el producto es un ingediente o un producto elaborado.

El código a utilizar dentro en la función 'replaceValuesColumn' (se puede dar el nombre que uno quiera) sería el siguiente:


private function replaceValuesColumn(
  item:Object, column:AdvancedDataGridColumn):String
{
  var result:String="";
  if (item.product_raw==true)
    result="Ingrediente";
  else if (item.product_raw==false)
    result="Elaborado";
  
  return result;
}


Resumiendo: los componentes, a la hora de rellenar cada dato en el componente, permiten utilizar una función para el tratamiento específico de dicho dato. Dicha función evalúa el dato y devuelve el valor que se ha de utilizar para visualizarlo.

En el caso de un AdvancedDataGrid, si hace uso de un agrupamiento, el tratamiento del item se complica, pues la estructura interna a la hora de agrupar varía.

Como ejemplo vamos ver el siguiente AdvancedDataGrid:



En este caso tenemos un agrupamiento por el campo "name_product_type" (tipo de producto), y además un campo resumen (Total). Al depurar la función, veremos que cuando se trata del agrupamiento, se crea un nivel de colección children con los datos del item en su interior, mientras que si trata directamente las filas de datos (ya agrupadas), este nivel no existe. Para tratar este caso, el código a utilizar sería el siguiente:


private function replaceValuesColumn(
  item:Object, column:AdvancedDataGridColumn):String
{
  var result:String="";
  
  try {
    if (item.product_raw==true)
      result="Ingrediente";
    else if (item.product_raw==false)
      result="Elaborado";
  }
  catch (e:Error) {
    if (item.children[0].product_raw==true)
      result="Ingrediente";
    else if (item.children[0].product_raw==false)
      result="Elaborado";
  }
  
  return result;
}


Safe Creative #1001195348518

lunes, 12 de octubre de 2009

Flex/AIR: Estilos personalizados para Alert

Los diálogos Alert de Flex/AIR no son componentes propiamente dichos, si no un objeto previamente definido por defecto en la interfaz de usuario, que se muestran como un PopUp. Este tipo de diálogo hereda los mismos atributos de visualización que el contenedor padre que lo invoca.

Es posible modificar sus atributos mediante CSS, de la siguiente manera:


<mx:Style>
  .alertTitle{
    color:#aaaaff;
    fontWeight:bold;
  }
  Alert {
    backgroundColor:#AAAAAA;
    backgroundAlpha: 0.8;
    color: #aaaaff;
    borderColor: #000000;
    borderAlpha: 0.8;
    headerHeight:25;
    themeColor: #666666;
    titleStyleName:alertTitle;
    dropShadowEnabled:true;
    shadowDirection:right;
    cornerRadius:10;
  }
</mx:Style>

Pero para que funcione, estos estilos no se deben definir desde un componente, si no desde la aplicación principal (Application en el caso de Flex, y WindowedApplication en el caso de AIR). Los estilos definidos aquí para Alert serán válidos para todos los Alert de la aplicación, en cualquier componente contenido.

Para más información sobre estilos que se pueden aplicar:
http://www.cristalab.com/tips/personalizar-diseno-del-componente-alert-de-flex-con-css-c47959l/

sábado, 3 de octubre de 2009

Flex/AIR: Imágenes en DataGrid

El ejemplo que hoy nos ocupa es muy sencillo y no pretende ambiciosos objetivos. Básicamente, permitirá a un DataGrid visualizar una imagen en una determinada celda que va vinculado al valor que lo contiene. Aunque esta entrada esté dedicada a un DataGrid, lo mismo puede aplicarse a las listas.

Para ilustrar el ejemplo, vamos a ver una captura del resultado final:


Como puede observarse, la columna Type (tipo) visualiza el tipo de lote (batch) mediante un simple gráfico: entrada (verde) o salida (rojo).

No voy a exponer cómo cargar los datos en el DataGrid, pues aquí, lo que nos ocupa es precisamente cómo cargar esas imágenes dependiendo del valor de la columna.

En este caso, sólo hay dos valores posibles, por lo que en la tabla de la base de datos sólo almacena en el campo "input_batch" los valores true o false, y para los mismos valores se han creado dos imágenes de 20x20, llamadas "true.png" y "false.png".

Por defecto, los DataGrids y las listas muestran la información en formato texto. Pero Flex/AIR puede permitir representar la información en formato gráfico. Para ellos existen lo que se denominan renderers (presentadores). Existen varios tipos de renderers, como cellRenderer o itemRenderer. Para nuestro ejemplo vamos a utilizar un itemRenderer de forma muy sencilla:

<mx:DataGrid id="dgBatches"
  dataProvider="{batchesList}"
  left="18" right="18" top="50" bottom="18">

  <mx:columns>
    <mx:DataGridColumn dataField="date" headerText="Date" width="80"/<
    <mx:DataGridColumn dataField="id_batch" headerText="Batches"/>
    <mx:DataGridColumn dataField="input_batch" headerText="Type" width="50">
      <mx:itemRenderer>
        <mx:Component>
          <mx:HBox width="100%" horizontalAlign="center">
            <mx:Image source="/resources/batches/{data.input_batch}.png" />
          </mx:HBox>
        /mx:Component>
      </mx:itemRenderer>
    </mx:DataGridColumn>
  </mx:columns>
</mx:DataGrid>

El punto clave está en la columna "input_batch", la cual recoge el tipo de lote (entrada (true) o salida (false). Esta columna contiene un elemento de tipo "itemRenderer", que es el encargado de presentar el valor de dicha celda. En su interior se crea un componente simple, compuesto únicamente por un HBox como contenedor, y como contenido tendrá una imagen que apuntará a la imagen a cargar. En este caso se encuetra en el directorio "/resources/batches/" y el nombre del archivo es el propio valor del campo (data.input_batch" con la extensión ".png".

Este mismo ejemplo podría servir para cargar imágenes ya predefinidas, en lugar de dos valores únicos de tipo true/false. También podrían residir en una URL concreta en lugar de en el disco duro.

Los renderers son muy flexibles y potentes, y permiten también introducir elementos de interfaz de usuario, como listas desplegables, campos numéricos, de texto, checkboxes, etc.

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.

Flex/AIR: filtros de datos

Es muy habitual tener una colección de datos en nuestras interfaces de usuario. Cuando ésta es muy numerosa, es conveniente facilitar al usuario de medios para poder acceder más rápida y directa a una información concreta, y para ello se aplican filtros.

En el ejemplo que voy a exponer utilizaré un DataGrid, que es uno de los elementos de interfaz más utilizados para presentar colecciones de datos. Lo mismo puede aplicarse a otros elementos de interfaz, como las listas o AdvancedDataGrids.

Antes de iniciar el cómo vamos realizar un planteamiento. La mayor parte de las veces, los datos provendrán de fuentes externas o remotas, como un fichero, un HTTPService o un WebService. La invocación a un servicio de datos remoto, normalmente se obtiene en formato e4x, para poder trabajar cómodamente en XML. Pero este sistema carece de la cualidad de filtrar o realizar búsquedas concretas. Se puede realizar filtros manualmente e invocar a los servicios de datos con los parámetros del filtro, pero esto implicaría desperdiciar muchos recursos, ya que cada filtrado es una invocación remota (una llamada a un servicio remoto), con el tiempo que esto conlleva, ejecutar el código de invocación, carga y visualización de datos.

Así pues, nos queda otra opción: obtener la información y almacenarlo en un ArrayCollection, que posee métodos para filtrado y que puede ser enlazado a elementos de interfaz de usuario. En este caso, los datos son los mismos que en la primera carga, e internamente, en memoria, se realiza el filtrado, por lo que es mucho más óptimo en recursos (tiempo, ejecuciones, memoria, etc.)

Para ello, se define un ArrayCollection en la región dedicado al ActionScript (<mx:Script>...</mx:Script>):

[Bindable]
private var miAC:ArrayCollection;


En este post se pondrá un ejemplo con un HTTPService, que es el método más habitual de obtener la información. Recordemos que HTTPService invoca a una URL donde se retornará una fichero XML formado con un nodo padre y con varios hijos en forma de lista (cada uno como un registro o fila del DataGrid). Esta invocación puede realizarse a una página ASP, JSP, PHP, Servlet, etc, que en lugar de retornar una página web retorne un XML. También puede ser una URL con un fichero XML.

<mx:HTTPService
  result="handleResultMetodo(event);"
  fault="handleFaultMetodo(event);"
  id="nombreServicio" resultFormat="object"
  url="URLHTTPService"
  useProxy="false">
  <mx:request xmlns="">
    <nombreParametro>valorParametro</nombreParametro>
    ...
  </mx:request>
</mx:HTTPService>

Los parámetros a utilizar son:
- result -> Nombre del método que gestionará el resultado de la invocación
- fault -> Nombre del método que se ejecutará en caso de haber un error en la invocación
- id -> Nombre que se da al servicio
- url -> URL donde se encuentra el XML (ASP, JSP, PHP, Servlet, fichero XML...)
- resultFormat -> Formato del resultado. En este caso se ha usado un tipo "object" para que sea recogido por el ArrayCollection. En casos normales se utiliza el formato "e4x" que permite gestionar cómodamente cualquier XML.

El bloque mx:request es opcional, y se utiliza en el caso de que el recurso XML requiera de parámetros de entrada para formar el XML resultante.

En algún momento determinado del ciclo de vida de la aplicación, se invocará al HTTPService para recoger el XML con los datos. Habitualmente se realiza en el método creationComplete de la aplicación, o del componente Flex. En algunas ocasiones se realiza en respuesta a algún evento (como un clic sobre un botón). El código que realiza la invocación sería el siguiente:

nombreServicio.send();

Una vez invocado el servicio HTTPService generará un resultado (en caso de ir bien) o bien un error (en caso de ir mal). En ambos casos se invocará a los métodos respectivos declarados:

private function handleResultMetodo(event:ResultEvent):void
{
  miAC = event.result.elementoPadre.elementoHijo;
}

private function handleFaultMetodo(event:FaultEvent):void
{
  Alert.show(event.fault.faultString, "ERROR");
}

Cuando la invocación ha sido exitosa se ejecutará el primer método, pasando en el parámetro "event" el resultado del XML. Para agregar este XML en el ArrayCollection, hay que acceder a la propiedad "result" del mismo, e indicar cuál es el elemento padre y cuál el elemento hijo (registro o fila). Por ejemplo, un XML con esta estructura:

<?xml version="1.0" encoding="ISO-8859-1" ?>

<producto>
  <item>
    <codigo>1</codigo>
    <nombre>Boligrafo negro</nombre>
    <precio>1.25</nombre>
  </item>
  <item>
    <codigo>2</codigo>
    <nombre>Boligrafo azul</nombre>
    <precio>1.50</nombre>
  </item>
  <item>
    <codigo>3</codigo>
    <nombre>Boligrafo rojo</nombre>
    <precio>1.75</nombre>
  </item>
<producto/>

El elemento padre sería "producto", y el elemento hijo sería "item". El ArrayCollection contendría 3 registros (uno por cada elemento hijo), y cada uno de estos registros sería una fila en el DataGrid.

El ArrayCollection está cargado en este momento con la información. El ArrayCollection definido es enlazable (tiene la propiedad Bindable), lo que permite o un control visual, como un DataGrid o un List, enlazarse a los datos de este ArrayCollection, y cada cambio en áquel actualiza automáticamente los datos del control.

El DataGrid se define así:

<mx:DataGrid
  id="miDataGrid">
  DataProvider="{miAC}">
    <mx:DataGridColumn dataField="codigo" headerText="CODIGO" />
    <mx:DataGridColumn dataField="nombre" headerText="NOMBRE" />
    <mx:DataGridColumn dataField="precio" headerText="PRECIO" />
</mx:DataGrid>

El control DataGrid es un control de datos en forma de tabla. El atributo "id" define el nombre para el DataGrid. El elemento "DataProvider" indica al DataGrid de dónde debe obtener los datos. En este caso, se indica entre llaves el nombre del ArrayCollection para que se lleve a cabo el enlace automático.

El DataGrid tiene tres columnas, que se definen con "DataGridColumn". El atributo "dataField" especifica el campo de donde va a obtener el valor ("codigo", "nombre" o "precio", definidos en el XML). El atributo "headerText" define el título que aparecerá en la cabecera superior de cada columna.

Ahora viene la parte interesante, y es el filtro. Para ello, se definen controles de usuario, como una caja de texto, una lista desplegable o un calendario para obtener una fecha. En cualquiera de los casos utilizados, se ha de invocar a una método cuando el valor de estos controles cambia (el que aplique según el caso). En este caso, vamos a poner que haya sido un campo de texto y un botón de anulación:

<mx:TextInput id="txtBuscar" change="buscarMetodo()" />
<mx:Button label="Reiniciar" click="reiniciarMetodo()" />

En el caso de que se introduzca un carácter en la caja de texto se producirá un cambio, que recogerá el evento "change", y aquí le indicamos que ejecute el método
buscarMetodo(), el cual contiene el siguiente código:

private function buscarMetodo():void {
  miAC.filterFunction = filtroMetodo;
  miAC.refresh();
}

El ArrayCollection tiene una propiedad que se encarga de la gestión de filtrado de los datos que contiene. Para ello, delega en un método que especifica cómo llevar a cabo este filtrado, y el cual, su nombre se especifica a esta propiedad, en este caso, "filtroMetodo". Este método contendrá el siguiente código:

private function filtroMetodo(item:Object):Boolean{
  var encontrado:Boolean = false;
  if(item.nombre.toLowerCase().search(txtBuscar.text.toLowerCase()) != -1){
    encontrado = true;
  }
  return encontrado;
}

El ArrayCollection ejecutará este método por cada fila que contiene, enviando en el objeto "item" la fila que se está procesando. Aquí se compara la fila ("item") y el campo a comparar (en este caso "nombre"), anmbos igualados en minúsculas. En caso de que case esta comparación se retorna un valor true (se muestra) o en caso contrario retorna un valor false (no se muestra).

Otra forma de comparación sería así:

private function filtroMetodo(item:Object):Boolean{
  var encontrado:Boolean = true;
  var filter:String = item["nombre"]; // valor del campo "nombre"
  if(filter.toLowerCase().indexOf(txtBuscar.text.toLowerCase())<0){
    encontrado = false;
  }
  return encontrado;
}

Una vez se ha procesado todas las filas e identificadas cuáles cumplen los requisitos del filtro, la siguiente sentencia:

  miAC.refresh();

Para anular los filtros y que el ArrayCollection (y por ende, el DataGrid) contenga toda la información, hay que anular la propiedad "filterFunction" y refrescar el Arraycollection:

private function reiniciarMetodo():void {
  miAC.filterFunction = null;
  miAC.refresh();
}


Refresca o actualiza el contenido del ArrayCollection, propagándose automáticamente hacia el DataGrid que actualizará la información a mostrar.

Un ejemplo muy sencillo es el siguiente:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="initData()">
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable]
private var dataList:ArrayCollection ;

private function initData():void{
dataList= new ArrayCollection([
{name:"Atlantic City Medical Center-City Division", city:"Atlantic City"},
{name:"Atlantic City Medical Center-Mainland Division", city:"Pomona "},
{name:"Barnert Hospital", city:"Paterson"},
{name:"Bayonne Medical Center", city:"Bayonne"},
{name:"Bayshore Community Hospital", city:"Holmdel"},
{name:"Bergen Regional Medical Center. L.P.", city:"Paramus"},
{name:"Burdette Tomlin Memorial Hospital", city:"Cape May"},
{name:"Capital Health System - Fuld Campus", city:"Trenton"},
{name:"Capital Health System - Mercer Campus", city:"Trenton"},
{name:"CentraState Healthcare System", city:"Freehold"},
{name:"Chilton Memorial Hospital", city:"Pompton Plains"},
{name:"Christ Hospital", city:"Jersey City"},
{name:"Clara Maass Medical Center", city:"Belleville"},
{name:"Columbus Hospital", city:"Newark"},
{name:"Community Medical Center", city:"Toms River"},
{name:"East Orange General Hospital", city:"East Orange"},
{name:"Englewood Hospital and Medical Center", city:"Englewood"},
{name:"Hackensack University Medical Center", city:"Hackensack"},
{name:"Hackettstown Community Hospital", city:"Hackettstown"},
{name:"Holy Name Hospital", city:"Teaneck"},
{name:"Hospital Center at Orange", city:"Orange"},
{name:"Hunterdon Medical Center", city:"Flemington"},
{name:"Irvington General Hospital", city:"Irvington"},
{name:"Jersey Shore University Medical Center", city:"Neptune"},
{name:"JFK Medical Center", city:"Edison"},
{name:"Kennedy Memorial Hospitals/UMC Cherry Hill", city:"Cherry Hill"},
{name:"Kennedy Memorial Hospitals/UMC Stratford", city:"Stratford"},
{name:"Kennedy Memorial Hospitals/UMC Washington Twp", city:"Turnersville"},
{name:"Kessler Memorial Hospital", city:"Hammonton"},
{name:"Kimball Medical Center", city:"Lakewood"},
{name:"LibertyHealth-Greenville Hospital Campus", city:"Jersey City"},
{name:"LibertyHealth-Jersey City Medical Center Campus", city:"Jersey City"},
{name:"LibertyHealth-Meadowlands Hospital Campus", city:"Secaucus"},
{name:"Lourdes Medical Center of Burlington County", city:"Willingboro"},
{name:"Monmouth Medical Center", city:"Long Branch"},
{name:"MONOC", city:"Eatontown"},
{name:"Morristown Memorial Hospital", city:"Morristown"},
{name:"Muhlenberg Regional Medical Center", city:"Plainfield"},
{name:"Newark Beth Israel Medical Center", city:"Newark"},
{name:"Newton Memorial Hospital", city:"Newton"},
{name:"Ocean Medical Center", city:"Brick"},
{name:"Our Lady of Lourdes Medical Center", city:"Camden"},
{name:"Overlook Hospital", city:"Summit"},
{name:"Palisades Medical Center-New York Presbyterian ", city:"North Bergen"},
{name:"Pascack Valley Hospital ", city:"Westwood"},
{name:"PBI Regional Medical Center", city:"Passaic"},
{name:"Raritan Bay Medical Center - Old Bridge", city:"Old Bridge"},
{name:"Raritan Bay Medical Center - Perth Amboy", city:"Perth Amboy"},
{name:"Riverview Medical Center", city:"Red Bank"},
{name:"Robert Wood Johnson Univ Hospital", city:"New Brunswick"},
{name:"Robert Wood Johnson Univ Hospital at Hamilton", city:"Hamilton"},
{name:"Robert Wood Johnson Univ Hospital at Rahway", city:"Rahway"},
{name:"Saint Barnabas Medical Center", city:"Livingston"},
{name:"Saint Clares Hospital/Boonton Township", city:"Boonton"},
{name:"Saint Clares Hospital/Denville", city:"Denville"},
{name:"Saint Clares Hospital/Sussex", city:"Sussex"},
{name:"Saint James Hospital", city:"Newark"},
{name:"Saint Michaels Medical Center", city:"Newark"},
{name:"Saint Peters University Hospital", city:"New Brunswick"},
{name:"Shore Memorial Hospital", city:"Somers Point"},
{name:"Somerset Medical Center", city:"Somerville"},
{name:"South Jersey Healthcare-Bridgeton Hospital", city:"Bridgeton"},
{name:"South Jersey Healthcare-Elmer Hospital", city:"Elmer"},
{name:"South Jersey Healthcare-Newcomb Hospital", city:"Vineland"},
{name:"Southern Ocean County Hospital", city:"Manahawkin"},
{name:"St. Francis Medical Center", city:"Trenton"},
{name:"St. Josephs Regional Medical Center", city:"Paterson"},
{name:"St. Josephs Wayne Hospital", city:"Wayne"},
{name:"St. Mary Hospital", city:"Hoboken"},
{name:"St. Marys Hospital Passaic", city:"Passaic"},
{name:"Summit Hospital", city:"Summit"},
{name:"The Cooper Health System", city:"Camden"},
{name:"The Memorial Hospital of Salem County", city:"Salem"},
{name:"The Mountainside Hospital", city:"Montclair"},
{name:"The Valley Hospital", city:"Ridgewood"},
{name:"Trinitas Hospital", city:"Elizabeth"},
{name:"Underwood-Memorial Hospital", city:"Woodbury "},
{name:"Union Hospital", city:"Union"},
{name:"University Medical Center at Princeton", city:"Princeton"},
{name:"University of Medicine &amp; Dentistry of NJ-Univ Hosp", city:"Newark"},
{name:"Virtua Memorial Hospital Burlington County", city:"Mount Holly"},
{name:"Virtua West Jersey Hospital-Berlin", city:"Berlin"},
{name:"Virtua West Jersey Hospital-Marlton", city:"Marlton"},
{name:"Virtua West Jersey Hospital-Voorhees", city:"Voorhees"},
{name:"Warren Hospital", city:"Phillipsburg"}
])
}

private function filterDemo():void{
dataList.filterFunction = searchDemo;
dataList.refresh();
}

private function searchDemo(item:Object):Boolean{
var isMatch:Boolean = false
if(item.name.toLowerCase().search(search.text.toLowerCase()) != -1){
isMatch = true
}
return isMatch;
}

private function clearSearch():void{
dataList.filterFunction = null;
dataList.refresh();
search.text = '';
}
]]>
</mx:Script>
<mx:Form>
<mx:FormItem label="Search" direction="horizontal">
<mx:TextInput id="search" change="filterDemo()" />
<mx:Button label="Clear Search" click="clearSearch()" />
</mx:FormItem>
</mx:Form>
<mx:DataGrid dataProvider="{dataList}" width="400" height="400">
<mx:columns>
<mx:DataGridColumn headerText="Name" dataField="name" />
<mx:DataGridColumn headerText="City" dataField="city" />
</mx:columns>
</mx:DataGrid>

</mx:Application>


En este caso, la información está en el propio código (por eso es tan extenso), y se carga directamente en el ArrayCollection sin usar un HTTPService. Pero el objetivo principal era demostrar el uso de los filtros.

Este ejemplo (el último) está en el siguiente enlace: http://www.boyzoid.com/blog/index.cfm/2006/10/19/Filtering-Data-in-Flex

Para terminar, indicar que los dos ejemplos expuestos anteriormente se han basado en un único campo, pero lo más normal es que un filtro pueda tener varios campos y se combinen entre sí.

A continuación voy a poner un ejemplo (no completo, si no únicamente en lo que concierne a este punto), con dos campos: uno en una combo y otro en un calendario.

El primero filtra un tipo de lotes. Sus valores pueden ser: * (todos), "I" (entrada o true) y "O" (salida o false).

Los dos campos de filtro, invocan a sus respectivos métodos cuando cambian su valor:

// User click on combo type of batch
private function changeType():void {
  if (cboTipoLote.value!="*" || datLote.text!=null)
    this.batchesList.filterFunction=filterBatches;
  else
    this.batchesList.filterFunction=null;

  this.batchesList.refresh();
}

// User change date on date field
private function changeDate():void {
  if (cboTipoLote.value!="*" || datLote.text!=null)
    this.batchesList.filterFunction=filterBatches;
  else
    this.batchesList.filterFunction=null;

  this.batchesList.refresh();
}

El ArrayCollection está definido en "batchesList".

El método delegado para el filtrado es el siguiente:

// Filter of batches
private function filterBatches(item:Object):Boolean
{
  var match:Boolean = true;
  var sDate:String="";
  var sType:String="";

  if (cboTipoLote.value=="I")
    sType="true";
  else if (cboTipoLote.value=="O")
    sType="false";
  else
    sType="";

  if (datLote.text !="") {
    sDate =""+datLote.selectedDate.fullYear +"-";
    var vValue:String=""+(datLote.selectedDate.month+1);
    if (vValue.length==1)
      vValue = "0" + vValue;

      sDate+= vValue + "-";

      vValue = ""+(datLote.selectedDate.date);
    if (vValue.length==1)
      vValue = "0" + vValue;

      sDate += vValue;
    }
    else
      sDate = "";

    var filter1:String = item["input_batch"];
    var filter2:String = item["date"];

    if (sDate!="" && sType=="") {
      if (!filter2 || filter2.indexOf(sDate)<0)
      match=false;
    }
    else if (sDate=="" && sType!="") {
      if (!filter1 ||
        filter1.toLowerCase().indexOf(sType.toLowerCase())<0)
        match=false;
    }
    else {
      if (!filter1 ||
        filter1.toLowerCase().indexOf(sType.toLowerCase())<0 ||
        !filter2 || filter2.indexOf(sDate)<0)
        match=false;
    }

  return match;
}

En este caso se contemplan 3 casos:
1) Se ha facilitado tipo pero no fecha
2) Se ha facilitado fecha pero no tipo
3) Se han facilitado tipo y fecha

Espero que estos ejemplos sean claros y útiles para desarrollar aplicaciones AIR y Flex más interactivas y optimizadas.