Reflexiones, artículos, opiniones, recomendaciones, noticias... sobre la tecnología.
miércoles, 30 de septiembre de 2009
Flex/AIR: Eventos personalizados
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
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:
<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 & 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.
viernes, 25 de septiembre de 2009
Configurar NetBeans, Tomcat y un pool de conexiones a PostgreSQL
PREPARATIVOS
El ejemplo que se ilustra aquí utiliza PostgreSQL 8.3 (http://www.postgresql.org, Java v6u14 (http://java.sun.com), Tomcat 6.0.20 (http://tomcat.apache.org) y NetBeans 6.7.1 (http://www.netbeans.org.
En primer lugar ha de estar instalado el SDK de Java y la base de datos PostgreSQL.
TOMCAT
Ahora, para instalar Tomcat, es preferible descomprimir la versión no instalable, ya que incorpora los archivos catalina.bat y catalina.sh, los cuales son gestionados por NetBeans para lanzar y depurar las aplicaciones. Yo recomiendo, para los usuarios de Windows, instalarlo en el directorio raíz del disco duro, pues en Windows Vista, Windows 7 y Windows Server 2008 utiliza el perfil de usuario para gestiones propias del entorno de sesión, y podría no funcionar.
Una vez instalado Tomcat, acceder al archivo conf/tomcat-users.xml y añadir el usuario administrador y el usuario manager:
<user username="admin" password="admin" roles="tomcat"/>
<user username="manager" password="manager" roles="manager"/>
El siguiente paso es descargarse el driver jdbc de PostgreSQL, el cual puede ser descargado de http://jdbc.postgresql.org. Descargar el driver correspondiente a jdbc3. Una vez descargado el archivo .jar, copiar éste en el directorio lib de Tomcat.
NETBEANS
Configuración del servidor
Una vez instalado NetBeans, hay que agregar el servidor Tomcat. Para ello:
- Menú "Tools" + Opción "Servers"
- Botón "Add Server..." (parte inferior izquierda)
- Seleccionar de la lista "Tomcat 6.0" y botón "Next"
- En el campo "Server Location", hacer clic en el botón "Browse..." y seleccionar la carpeta donde se aloja Tomcat.
- En el campo "Username" ingresar el nombre del usuario "manager"
- En el campo "Password" ingresar la contraseña del usuario "manager"
- Botón "Finish"
El servidor se habrá agregado a la lista de servidores configurados. Al seleccionar este servidor, aparecerán sus propiedades a la derecha. Clic en el botón "Close".
Crear proyecto web
Vamos a crear un proyecto web para configurar el pool de conexiones y mostrar un ejemplo de conexión:
- Menú "File" + opción "New Project..."
- En la lista "Categories" seleccionar "Java Web", y en la lista "Projects" seleccionar "Web Application"
- Botón "Next"
- En el campo "Project Name" dar el nombre del proyecto, por ejemplo "Prueba".
- Botón "Next"
- En el campo "Server" seleccionar "Tomcat 6.0"
- Botón "Finish"
Referencia a la librería de PostgreSQL
Creado el proyecto, lo primero que vamos a hacer es añadir una referencia a la librería de PostgreSQL. Para ello, en el panel "Project" (parte izquierda), aparecerá el proyecto con los diversos elementos del mismo. Hacer clic con el botón derecho sobre "Libraries" y seleccionar "Add JAR/Folder". Navegar hasta el directorio lib de Tomcat y seleccionar el archivo .jar de PostgreSQL. La librería aparecerá en el árbol subyacente.
Configurar pool de conexiones
A continuación vamos a configurar el pool de conexiones. Antes de empezar, comentar que en la versiones recientes de Tomcat, por motivos de seguridad y eficiencia, la configuración de contextos y recursos se realiza por aplicación, y en no en el contexto general de Tomcat. Aclarado ésto vamos a proceder a la configuración.
Desplegar, en el panel de proyecto, el elemento "Web pages" del proyecto. Desplegar ahora el elemento "WEB-INF" y hacer doble clic sobre el archivo "web.xml". Seleccionar ahora el botón "XML" (parte superior de la zona de trabajo), con lo que aparecerá el contenido del archivo. Aquí aparecerá un bloque principal encerrado entre los tags <web-app> y </web-app>. Situarse justo antes del cierre de este bloque y añadir las siguientes líneas:
<resource-ref>
<description>Referencia al pool de conexiones</description>
<res-ref-name>jdbc/[nombreJNDI]</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
El campo <res-ref-name> contiene el nombre JNDI que se utilizará como contexto para el pool de conexiones.
El siguiente paso es acceder al elemento "Web pages" del proyecto. Desplegar ahora el elemento "META-INF" y hacer doble clic sobre el archivo "context.xml". Se mostrará el contenido:
<Context antiJARLocking="true" path="/TrazalogicOperator"/>
Modificar esta línea para definir dos líneas (apertura y cierre) en lugar de una:
<Context antiJARLocking="true" path="/[NombreProyecto]">
</Context>
[NombreProyecto] es asignado automáticamente por NetBeans, y no es necesario cambiarlo.
Entre estas dos líneas añadir lo siguiente para crear el contexto del pool:
<Resource name="jdbc/[nombreJNDI]"
auth="Container"
type="javax.sql.DataSource"
username="[usuarioPostgreSQL]"
password="[passwordPostgreSQL]"
driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://[servidor]:[puerto]/[nombrebasedatos]"
maxActive="8"
maxIdle="4"/>
Un ejemplo de url sería:
url="jdbc:postgresql://localhost:5432/miBaseDatos"
Si la base de datos va a residir en la misma máquina que Tomcat y la aplicación web, se utilizaría "localhost". Si estuviera en otra máquina, especificar el nombre de la misma (si está configurado en un servidor DNS) o bien la dirección IP de la misma.
El puerto 5432 es el puerto por defecto de la base de datos PostgreSQL. Si se ha configurado otro puerto, modificar este parámetro.
Probar todo
Para probar tanto el servidor de aplicaciones como el pool de conexiones, vamos a crear un Servlet. Para ello:
- Menú "File" + opción "New File..."
- En la lista "Categories" seleccionar "Web", y en la lista "File Type" seleccionar "Servlet".
- Botón "Next"
- En el campo "Class Name" escribir el nombre del servlet, por ejemplo "ServletPrueba".
- En el campo "Package" escribir el nombre del paquete donde se ubicará el servlet, por ejemplo "com.prueba.servlet"
- Botón "Next"
- Botón "Finish"
Se creará el servlet con un código mínimo.
En la sección "import" añadir las siguientes importaciones:
import java.sql.*;
import javax.sql.*;
import javax.naming.*;
Buscar el método processRequest y escribir el siguiente código:
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet ServletPrueba</title>");
out.println("</head>");
out.println("<body>");
out.println("INICIO...<br>");
Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
DataSource ds = (DataSource)
envCtx.lookup("jdbc/[nombreJNDI]");
Connection conn = ds.getConnection();
//... Insertar aquí las consultas y actualizaciones a base de datos
conn.close();
out.println("...FIN<br>");
out.println("</html>");
} catch (Exception ex) {
out.println("Error: " + ex.toString();
} finally {
out.close();
}
}
A través del nombre JNDI se extrae el contexto para el pool de base de datos, retornando un objeto de tipo DataSource. Una vez obtenido el DataSource, es posible obtener la conexión (método getConnection()). A partir de ahí, con la conexión, procederemos a realizar las consultas a base de datos, o las actualizaciones pertinentes.
El código, tal y como está, servirá para verificar que se obtiene la conexión. Si todo va bien, devolverá una página Web, con el único texto "INICIO... / ...FIN". Si hubiera algún error, retornaría también el error producido.
Para ejecutar este servlet, en el panel de proyecto (parte izquierda), abrir la carpeta "Source Packages" + "com.prueba.servlet", y con el botón derecho hacer clic sobre el elemento "ServletPrueba.java" y seleccionar "Run File" (para ejecutar el servlet) o "Debug File" (para depurar el servlet, previo marcado un punto de interrupción).
Antes de finalizar, comentar qué me ha llevado a escribir este post, tras tres días devanándome la cabeza con Eclipse y con GlassFish, con los que he tenido multitud de problemas y una complejidad bastante elevada en cuanto a una configuración para trabajar. Soy de los que piensan que tenemos ya mucho trabajo por delante como para perder el tiempo descifrando o poniéndote delante de una bola de cristal para intentar explicar cómo configurar algo que debería ser automático y simple, y que en apenas unos segundos y de manera intuitiva debería dejarte empezar a trabajar. Ahí radica el éxito de Visual Studio de Microsoft, que facilita el trabajo multiplicando la productividad.
Con Eclipse, de forma inexplicable, los cambios en mi servlet no se actualizaba ni desplegaba en el servidor. Por más que he intentado saber por qué, aún no me lo explico.
Con GlassFish, que parece un servidor potente y muy bien diseñado, veía muchos pasos para configurar el pool de conexiones (primero una conexión, luego el pool y luego el resource, y luego configurar el context.xml y el web.xml). Pero aún así, mirando la documentaciónd e GlassFish, PostgreSQL no venía homologado en las especificaciones, aunque en realidad sí que permitía su configuración, y la conexión, de hecho me permitía mirar la estructura de la base de datos y ejecutar consultas desde el IDE. Pero a la hora de desarrollar, el DataSource era imposible de capturar, y desde el propio GlassFish, probar el DataSource me daba un error FATAL por que database devolvía null. El problema es que el driver de conexión es peculiar en PostgreSQL, y se obtiene mediante un DataSource "especial" llamado org.postgreSQL.ds.PGSimpleDataSource, el cual es incompatible o no se puede convertir a un DataSource normal. Asimismo, incluso intentando obtener un PGSimpleDataSource en lugar de un DataSource, también daba error porque internamente trabaja con DataSource. Vamos un galimatías que me hizo desistir.
Al final, me empapé la documentación de Tomcat (concretamente http://localhost:8080/docs/jndi-resources-howto.html) y trasteando con Tomcat, conseguí definir este procedimiento de configuración, simplificándolo al máximo y que funciona.
Espero que os sea de utilidad.
martes, 22 de septiembre de 2009
Visita al SIMO 2009
Empieza las 10:00. Personas fuera esperando. En el interior hay escáners para bolsas, paquetes y equipajes, como en el aeropuerto. También hay arcos detectores, y bandejas para dejar los objetos metálicos. Todo lleno de policias nacionales y algún que otro trajeado con "pinganillo", al estilo de la CIA o del FBI. Esto parece de película. ¿Será por seguridad o sólo porque el principito Felipe va a venir, seguramente con la corte de pelotas como Gallardón (alcalde de Madrid) y la Espe (presidenta de la Comunidad de Madrid?. No lo sé, pues no me quedé a esperarles.
Para entrar y salir en todos los pabellones y accesos, hay que pasar la tarjeta de identificación (código de barras en 2D y 3D) por un lector para el torniquete.
Primera sorpresa, vas andando desde el pabellón 1, y andas y andas, y andas, y todos los pabellones, a izquierda y derecha CERRADOS!!!! Llegamos al final, y el número 7 está abierto.
¡¡¡ Sorpresa !!! El pabellón 7 es muy diáfano, con pocos stands, muy separados y... adividinad: ¿quién ocupa más de la mitad del pabellón?. Nada más y nada menos que Microsoft, presentando su Ventanucos 7, su Exchange 2008, el Office y el Dynamics (por cierto, una pasada viendo cómo funciona en una mesa de pantalla táctil).
Pruebo algún que otro equipo funcionando con Ventanucos 7. Nada mal, acostumbrado a mi Vista. Parece (a primer a vista) más estable, robusto y funcional. Mi feeling es bueno.
Junto a Microsoft hay alguna que otra consultora de las que andan, como las rémoras, junto al gran tiburón blanco. Curiosamente, está Sun Microsystems con un stand muy pequeño, promocionando únicamente hardware, como sistemas de almacenamiento, y algún servidor modular. Una cosa extrañamente curiosa, y al final os contaré mi reflexión.
Bueno, vamos al pabellón 9, hay bastantes stands pequeñitos, como los de las ferias de pueblo. Los más pudientes tiene algún stand un poco más grande, como Acer o Airis. Pero los más grande pertenecen a SAP, HP, DELL (en un camión tráiler de color negro) y a la editorial que edita MacWorld. Alguno que otro ha contratado a un maestro cortador de jamón, que sabe rancio en esta feria (no por el jamón, es una ironía). Sorprendentemente, cerca de un cuarto del espacio de este pabellón está vallado y desaprovechado.
Salimos fuera y nos encontramos que el resto de pabellones al otro lado están cerrados. ¡No puede ser! ¿Cómo va a haber sólo dos pabellones para el SIMO?. Volvemos a entrar y preguntamos. ¡SOLO HAY DOS PABELLONES! Algo que ves en apenas una hora y media.
En Telefonía está Vodafone y Telefónica. Pero no hay fabricantes de teléfonos móviles, como Nokia, Panasonic, Samsung o Motorla, como en otras ediciones. Sólo hay compañías de telefonía, y sólo las más grandes (echo en falta a Symio, Yoigo u Orange).
En software están los de SAP y los más conocidos en ERP y en CRM. Pero empiezo a echar en falta a los grandes fabricantes de software y de bases de datos (como Oracle). No han acudido Adobe, ni Java, ni empresas de software libre (sólo encontré Opentrad, con un software de traducción libre, y Cenatic, de forma muy discreta), ni Hispalinux, ni la comunidad de Extremadura (con su Linex), ni la Junta de Andalucia... ¿DONDE ESTAN?
Pregunto a unos amigos que tenían un stand en la feria (hace años trabajé con ellos), y me chistan para que baje la voz y no pronuncie la palabra maldita: software libre. Este gesto me mosquea. ¿No hay pasta para estos fabricantes debido a la crisis? ¿La organización del SIMO ha hecho algo por lo que estos fabricantes no estén de acuerdo y no hayan asistido? ¿O acaso hay una política discriminatoria con respecto al software libre? Lo ignoro, pero el retorno del SIMO es agridulce ante esta perspectiva desoladora.
Me quedan todas las dudas del mundo acerca de lo que Oracle va a hacer con los productos de Sun Microsystems, como Java, MySQL o StarOffice. Me quedan dudas sobre qué hace ahora Borland que ha sido otra vez adquirida. Se hablaba de un Delphi 2010, pero me quedo con las ganas. Me quedo con las ganas también de conocer alternativas a estos tiburones, sean o no libres. Opentrad (que por cierto, el comercial que me atendió fue amabilísimo, me explicó todo muy bien e incluso me invitó a un zumo de naranja recién exprimido), era el único fabricante de software libre, pero no competía con nadie de la feria en su terreno. Lo mismo ocurría con algunos fabricantes de hardware humildes. Se pone de manifiesto el abismo entre ricos y pobres, las apariencias y la publicidad que ponen ante la diferencia de imagen, los que tienen un poder abusivo y que no permiten la competencia.
Mi opinión es que el SIMO ha vuelto, y es un buena noticia, pues se vuelve a reactivar el mercado y hay esperanzas de volver a la fertilidad tecnológica. Por otro lado, el sabor amargo ante la poca oferta que imponen, la diferencia abismal entre tiburones, rémoras y gorriones que pican las migajas, y que el software libre haya sido eliminado del directorio de la feria, siendo hoy en día uno de los principales candidatos a un nuevo mercado rico en ideas y en competitividad. Está visto que el SIMO pertenece a cuatro gigantes que no deja crecer la hierba bajo su sombra.
Me fui de la feria dos horas y cuarto después de haber llegado, desazonado y triste. No tuve ni ganas de ver al principito y su corte de pelotas con afán de cámara y publicidad falsa. Para colmo, las dos horas y cuarto de parking me salió por 4,15 euros. TODO UN ROBO!!!!
lunes, 21 de septiembre de 2009
TI Facturas listo para su descarga
Descarga y documentación:
http://www.tecnillusions.com
sábado, 19 de septiembre de 2009
Base de datos bloqueada en SQLite y .NET
La principal razón de que esto ocurra es que SQLite trabaja en una única sesión, por lo que la concurrencia queda descartada para grandes pretensiones. Incluso con esto en mente, trabajando en una aplicación de escritorio (no en servidor), esto puede suponer también un problema cuando queremos trabajar con varias consultas anidadas y actualizaciones.
En este post voy a contar mi experiencia y cómo he capeado este problema.
Ante todo, hay que reutilizar el código, por lo que el acceso a base de datos la realizo desde una clase con métodos públicos. En esta clase defino los objetos clave para el uso de la base de datos:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SQLite; // No olvidar esta referencia
// Clase específica para acceso y carga de datos desde la base de datos
namespace TI_Facturas
{
class DatosManager
{
private SQLiteConnection con = null;
private SQLiteCommand cmd = null;
private SQLiteDataReader dtr = null;
}
}
Los objetos deben tener un uso exclusivo cada vez, por lo que se definen dos métodos: uno para iniciarlos y otro para despacharlos.
// Inicializa los objetos de base de datos
private void initSQL()
{
con = null;
cmd = null;
dtr = null;
con = new SQLiteConnection("Data Source=ti_facturas.db;Version=3;New=False;Compress=True;");
con.Open();
}
// Prepara la clase para ser despachada
public void dispose()
{
if (cmd != null)
cmd.Dispose();
if (dtr != null)
{
dtr.Close();
dtr.Dispose();
}
if (con != null)
{
con.Close();
con.Dispose();
}
con = null;
cmd = null;
dtr = null;
GC.Collect();
}
El método initSQL() ha de invocarse antes de empezar cualquier operación con la base de datos. A continuación se realizan las operaciones con la base de datos (accesos y actualizaciones). Y por último se invoca al método dispose() para finiquitar los objetos.
La sentencia GC.Collect() permite vaciar el Garbage Collector, un repositorio que .NET utiliza para depositar los objetos que dejan de ser utilizados. Es posible que en algunas ocasiones, aunque cerremos convenientemente los objetos para despacharlos al Garbage Collector, éste aún pueda tener la referencia a SQLite y no haya procesado su limpieza automática, por lo que nos dará, inevitablemente, el conocido error de "database locked", a pesar de ser meticulosos y despachar a medida que dejamos de utilizar.
Recomiendo utilizar en esta clase los métodos de acceso a la base de datos, e ir invocándolo desde nuestras propias clase. Por ejemplo, ahí van las más utilizadas de forma genérica:
// Ejecuta una sentencia de actualizacion (NO SELECT)
public String executeNonQuery(string sql)
{
String result = "" ;
try
{
initSQL();
cmd = new SQLiteCommand(sql, con);
int rows = cmd.ExecuteNonQuery();
if (rows == 0)
result = "No se ha realizado ningun cambio [" + sql + "]";
}
catch (Exception argEx)
{
result = "Error al ejecutar SQL ["+sql+"]: " + argEx.Message;
}
dispose();
return result;
}
// Obtiene el valor de un unico campo para una consulta de un solo registro
public String getUniqueValue(string sql)
{
String result = "";
try
{
initSQL();
cmd = new SQLiteCommand(sql, con);
dtr = cmd.ExecuteReader();
dtr.Read();
result = (String)dtr[0].ToString();
}
catch (Exception argEx)
{
result = "";
}
dispose();
return result;
}
// Obtiene el conjunto de registros de una consulta SQL Select
public SQLiteDataReader executeReader(string sql)
{
try
{
initSQL();
cmd = new SQLiteCommand(sql, con);
dtr = cmd.ExecuteReader();
}
catch (Exception argEx)
{
dtr = null;
}
return dtr;
}
EJEMPLOS DE USO:
Ejemplo 1: Actualización
string sql="insert into paises (id_pais, pais) values (1, 'Spain')";
DatosManager dm = new DatosManager();
dm.executeNonQuery(sql);
dm.dispose();
Ejemplo 2: Consulta campo especifico en registro unico
string sql="select nombre from clientes where id_cliente=32";
DatosManager dm = new DatosManager();
string nombre=dm.getUniqueValue(sql);
dm.dispose();
Ejemplo 3: Consulta con conjunto de varios registros
string sql="select id_provincia, provincia from provincias order by provincia";
DatosManager dm = new DatosManager();
SQLiteDataReader dr=dm.executeReader(sql);
while (dr.Read())
{
// Tratamiento de cada registro
}
dr.Close(); // Despachar primero el DataReader
dr.Dispose();
dm.dispose();
jueves, 17 de septiembre de 2009
TI_Facturas v2009 Beta
Entre sus características cabe destacar las siguientes:
* Sencillo e intuitivo, lo que tiene un período muy breve de aprendizaje.
* Rápido, eficaz y ligero en recursos.
* Gestión de múltiples empresas.
* Numeración automática de presupuestos y de facturas, independientemente de la empresa.
* Gestión de empresas.
* Gestión de clientes.
* Internacionalidad en empresas y clientes.
* Configuración de unidades métricas a ser utilizadas en los conceptos.
* Configuración de las formas de pago de las facturas.
* Generación de facturas a partir de los presupuestos aceptados.
* Configuración de los impuestos y descuentos a aplicar de forma genérica (por empresa), o individual (por factura).
* Registro de plazos de pago
* Libertad en la redacción de los conceptos:
o Varias líneas por concepto.
o Texto extenso, que automáticamente es distribuido en varias líneas.
o Cantidad, Unidad, Precio y Total, libres y opcionales de mostrar.
o Cálculo automático del Total si Cantidad y Precio son introducidos.
* Autopaginación. Los totales se visualizan en la última página, y cada página es enumerada.
* Posibilidad de agrupar facturas en una factura, formando parte del concepto de la misma.
* Potente filtro con combinatoria de criterios de búsqueda:
o Por empresa
o Por cliente
o Por una fecha determinada
o Por un rango de fechas (desde...hasta)
o Aceptado (presupuesto) o pagada (factura)
* Configuración de logotipo para su publicación en presupuestos y facturas.
* Alarmas de plazos vencidos.
* Sistema operativo Windows XP, 7 o Vista.
* Sin licencias por máquina: se puede instalar en cuantas máquinas se desee, teniendo en cuenta que los datos de cada una es independiente de la otra.
* Modalidades:
o Gratuito: Completamente funcional, con algunas limitaciones.
o Premium:
+ Sin limitaciones
+ Soporte y garantía de 1 año contra defectos.
+ Servicio de actualización gratuito.
+ Por sólo 150 euros
* Soporte adicional y opcional (previo presupuesto)
* Personalización a medida (previo presupuesto):
o Funcionalidades a medida
o Traducción al idioma requerido
o Personalización del presupuesto
+ Modelo
+ Tipografía
+ Distribución
o Personalización de la factura
+ Modelo
+ Tipografía
+ Distribución
Para solicitar la versión gratuita para probar TI_Facturas sin compromiso enviar un correo a info@tecnillusions.com
Capturas de pantalla:
Consultas SQLite desde .NET
El primer paso a dar es instalarse el conector System.Data.SQLite.dll (se puede descargar de http://sqlite.phxsoftware.com). Una vez instalado, hay que hacer referencia a dicho conector mediante Project > Add Reference... (Proyecto > Añadir referencia...), buscar el conector y OK (Aceptar).
El siguiente paso es añadir al inicio del código el uso de las clases de esta librería:
using System.Data.SQLite;
Los ejemplos que aquí se muestran están escritos en C#, pero la adaptación a VB.NET no debería suponer gran transcendencia.
En primer lugar hay que encerrar todo el proceso de acceso a la base de datos en un bloque try...catch. Dentro de este bloque comenzaremos abriendo la conexión, mediante:
SQLiteConnection sql_con = new SQLiteConnection ("DataSource=ARCHIVOBBDD;
Version=3;New=False;Compress=True;");
sql_con.Open();
donde, ARCHIVOBBDD es el nombre del archivo de la base de datos. Si este archivo se encuentra en una ruta fija, hay que indicar toda la ruta. Si se escribe únicamente el nombre del archivo, éste debería estar ubicado en el directorio debug y release del proyecto.
A continuación se crea el comando de la sentencia a ejecutar (en este caso una consulta o SELECT):
string CommandText = "select id_empresa, nombre from EMPRESAS order by nombre";
SQLiteCommand sql_cmd = new SQLiteCommand(CommandText, sql_con);
El siguiente paso será ejecutar el comando para obtener el resultado de la consulta, el cual estará en un DataReader:
SQLiteDataReader sql_dtr = sql_cmd.ExecuteReader();
En este momento, el puntero del cursor se encuentra justo antes del primer registro. Con el método Read(), se obtenerá el siguiente registro o fila, retornando true si se leyó correctamente, o false si no se leyó, en cuyo caso se habrá alcanzado el final del resultado. La lectura de todos los registros se puede realizar dentro de un bucle como éste:
while (sql_dtr.Read())
{
... // Bloque para tratar los campos de la fila obtenida
}
El acceso a cada campo se realiza a modo de array con el DataReader, comenzando desde 0, que sería el primer campo, hasta n-1. También se podría acceder indicando dentro del elemento del array el nombre del campo entre comillas, en lugar del índice. Las dos siguientes líneas realizan lo mismo:
string nombreEmpresa = (string)sql_dtr[1];
string nombreEmpresa = (string)sql_dtr["nombre"];
En todos los casos hay que hacer un casting del tipo de dato (anteponiendo y encerrando entre paréntesis el tipo de dato), por lo que hay que tener especial cuidado con los datos que son numéricos o de fecha. Las siguientes líneas son una muestra de ello, aunque no tengan que ver con el ejemplo que estamos planteando:
long lData = (Int64)sql_dtr[0]; // Tipo entero largo
Single sData = (Single)sql_dtr[1]; // Tipo decimal
Double dData = (Double)sql_dtr[0]; // Tipo decimal de doble precisión
Para hacer una captura correcta de datos, lo mejor es comprobar si el dato no es nulo y un parseo en consecuencia, como en el siguiente ejemplo:
Single sCant=0;
string sData = dtr[0].ToString();
if (sData != null && sData != "")
sCant = Single.Parse(sData);
Si el dato no admite nulos, se puede realizar el parseo directamente:
long plazo = (long)long.Parse(sql_dtr[6].ToString()) ;
Double dData = Double.Parse(sql_dtr[0].ToString());
DateTime dtFecha = DateTime.Parse(sql_dtr["fecha"].ToString());
Una vez se han leído los registros y no se vaya a utilizar más el DataReader, hay que cerrarlo para optimizar recursos:
sql_dtr.Close();
Si la conexión ya no es necesaria por el momento, cerrarla también:
sql_con.Close();
El código completo del ejemplo, es el siguiente, y su objetivo es cargar una lista de empresas en una combo (lista desplegable):
// Carga la combo de empresas
private void loadEmpresas()
{
try
{
SQLiteConnection sql_con = new SQLiteConnection
("Data Source=ti_facturas.db;Version=3;
New=False;Compress=True;");
sql_con.Open();
string CommandText =
"select id_empresa, nombre from EMPRESAS order by nombre";
SQLiteCommand sql_cmd = new SQLiteCommand(CommandText, sql_con);
SQLiteDataReader sql_dtr = sql_cmd.ExecuteReader();
DataTable dt;
dt = new DataTable("Datos");
dt.Columns.Add("id_empresa");
dt.Columns.Add("empresa");
DataRow dr;
while (sql_dtr.Read())
{
dr = dt.NewRow();
dr["id_empresa"] = ((Int64)sql_dtr[0]);
dr["empresa"] = (string)sql_dtr[1];
dt.Rows.Add(dr);
}
cboEmpresa.DataSource = dt;
cboEmpresa.ValueMember = "id_empresa";
cboEmpresa.DisplayMember = "empresa";
sql_dtr.Close();
sql_con.Close();
}
catch (Exception argEx)
{
Console.WriteLine("Exception message: " + argEx.Message);
}
}