28 septiembre 2007

La potencia de los ClientDataSet (III)

Después de crear la base de datos y la capa de acceso a datos dentro del DataModule vamos a crear la lógica de negocio.

Antes de seguir tenemos que hacer doble clic en los componente IBQuery de la capa de acceso a datos y pulsar la combinación de teclas CTRL + A para introducir todos los campos en el módulo de datos. Y en cada una de ellas hay que seleccionar el campo ID y quitar la propiedad Required ya que el propio motor de bases de datos va a meter el campo ID con un disparador.

Pero tenemos un problema, y es que no hemos seleccionado donde esta la base de datos. Para ello hacemos doble clic en el objeto BaseDatos situado módulo de datos AccesoDatos. Seleccionamos una base de datos Remota (Remote) con IP 127.0.0.1. Y la base de datos donde la tengamos, por ejemplo:

D:\Desarrollo\Delphi7\ClientDataSet\BaseDatos.fdb

De este modo, al hacer CTRL + A sobre las tablas conectará automáticamente sobre la base de datos para traerse los campos. No se os olvide luego desconectarla.

CREANDO LA LOGICA DE NEGOCIO

La capa de lógica de negocio también la vamos a implementar dentro de un objeto DataModule y va a constar de los siguientes componentes:


- Un DataModule llamado LogicaNegocio.

- Dos componentes DataSetProvider llamados DSPLstClientes y DSPClientes.

- Dos componentes ClientDataSet llamados TLstClientes y TClientes.

Ahora vamos a configurar cada uno de estos componentes:

- Enlazamos el DataModule LogicaNegocio con el DataModule AccesoDatos en la sección uses.

- Al componente DSPLstClientes le asignamos en su propiedad DataSet el componente AccesoDatos.LstClientes.

- Al componente DSPClientes le asignamos en su propiedad DataSet el componente AccesoDatos.Clientes.

- El componente TLstClientes lo vamos a vincular con DSPLstClientes mediante su propiedad ProviderName.

- El componente TClientes lo vamos a vincular con DSPClientes mediante su propiedad ProviderName.

- Debemos hacer doble clic en ambos ClientDataSets y pulsar la combinación de teclas CTRL + A para meter todos los campos.

CONTROLANDO EL NUMERO DE REGISTROS CARGADOS EN MEMORIA

Los componentes ClientDataSet tienen una propiedad llamada PacketRecord la cual determina cuandos registros se van a almacenar en memoria. Por defecto tiene configurado -1 lo que significa que se van a cargar todos los registros en la tabla. Como eso no me interesa en el listado general del formulario principal lo que vamos a hacer es poner esta propiedad a 100.

Por eso he ordenado la lista por el campo ID descendentemente para que se vean sólo los últimos 100 registros insertados. Una de las cosas que más me gustan de los componentes ClientDataSet es que se trae los 100 últimos registros y desconecta la tabla y la transacción quitándole trabajo al motor de bases de datos. Si el usuario que maneja el programa llega hasta el registro número 100 el propio componente conecta automáticamente con el servidor, se trae otros 100 registros y vuelve desconectar.

Lo único en lo que hay que tener cuidado es no acumular demasiados registros en memoria ya que puede relentizar el programa e incluso el sistema operativo si el PC no tiene mucha potencia.

El componente ClientDataSet llamado TClientes lo dejamos como está en -1 ya que sólo lo vamos a utilizar para dar de alta un registro o modificarlo.

ENVIANDO LAS TRANSACCIONES AL SERVIDOR

Cuando se utilizan los clásicos métodos Insert, Append, Post y Delete con los objetos de la clase TClientDataSet el resultado de las operaciones con registros no tiene lugar en la base de datos hasta que envíamos la transacción al servidor con el método ApplyUpdates.

Por ello vamos a utilizar el evento OnAfterPost para enviar la transacción al servidor en el caso que haya sucedido alguna modificación en el registro:

procedure TLogicaNegocio.TClientesAfterPost( DataSet: TDataSet );
begin
if TClientes.ChangeCount > 0 then
begin
TClientes.ApplyUpdates( 0 );
TClientes.Refresh;

if TLstClientes.Active then
TLstClientes.Refresh;
end;
end;

Después de enviar la transacción hemos refrescado la tabla TClientes y también la tabla TLstClientes para que se actualicen los cambios en la rejilla. Por último, cuando en el listado de clientes se elimine un registro también hay que enviar la trasacción al servidor en su evento OnAfterDelete:

procedure TLogicaNegocio.TLstClientesAfterDelete( DataSet: TDataSet );
begin
TLstClientes.ApplyUpdates( 0 );
end;

Con esto ya tenemos controlada la inserción, modificación y eliminación de registros hacia el motor de bases de datos.

ESTABLECIENDO REGLAS DE NEGOCIO EN NUESTRAS TABLAS

Las reglas de negocio definidas en el módulo de datos le quitan mucho trabajo al formulario que esté vinculado con la tabla. En un primer ejemplo vamos a hacer que cuando se demos de alta un cliente su importe pendiente sea cero. Esto se hace en el evento OnNewRecord del componente TClientes:

procedure TLogicaNegocio.TClientesNewRecord( DataSet: TDataSet );
begin
TClientesIMPORTEPTE.AsFloat := 0;
end;

Otra de las malas costumbres que solemos cometer en los programas es controlar los datos que introduce o no el usuario en el registro, cuya lógica la hacemos en el formulario. Esto tiene el inconveniente en que si en otro formulario hay que acceder a la misma tabla hay que volver a controlar las acciones del usuario.

Para evitar esto, tenemos que definir también en la capa de lógica de negocio las reglas sobre las tablas y los campos, lo que se llama comunmente validación de campos. El componente ClientDataSet dispone de la propiedad Constraints donde pueden definirse tantas reglas como queramos. Para verlo con un ejemplo, vamos a hacer que el usuario no pueda guardar el cliente si no ha introducido su nombre.

Para definir una regla en un ClientDataSet hay que hacer lo siguiente (con TClientes):

- Pulsamos el botón [...] en la propiedad Constraints.

- Pulsamos el botón Add New.

- En la propiedad CustomConstraint definimos la condición de error mediante SQL:

NOMBRE IS NOT NULL

- En el campo ErrorMessage del mismo Constraint ponemos el mensaje de error:

No ha introducido el nombre del cliente

Con esta regla definida, si en cualquier parte de nuestro programa hacemos un Post de la tabla clientes y esta vacío el campo NOMBRE el programa lanzará un mensaje de error sin tener que programar nada. Antes teníamos que hacer esto en el botón Aceptar del formulario para validar los campos. Con los Constraints las validadiones las hacemos en sin tener que programar.

Ahora vamos a definir otra regla la cual establece que un cliene no puede tener un importe pendiente superior a 2000 €. Creamos un nuevo Constraint con la propiedad CustomConstrait definida con:

IMPORTEPTE <= 2000

y con la propiedad ErrorMessage que tenga:

El importe pendiente no puede ser superior a 2000 €

Se pueden definir tantas reglas como deseemos. En el próximo artículo vamos a hacer los formularios de mantenimiento de clientes.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

27 septiembre 2007

La potencia de los ClientDataSet (II)

Después de haber creado la base de datos en Firebird 2.0 ya podemos comenzar a crear un nuevo proyecto que maneje dicha información. El objetivo del proyecto es hacer el siguiente mantenimiento de clientes:


CREANDO LA CAPA DE ACCESO A DATOS

La capa de acceso a datos encargada de conectar con Firebird va a incorporar los siguientes componentes:


- Un contenedor DataModule llamado AccesoDatos.

- Un compomente IBDatabase (pestaña Interbase) llamado BaseDatos.

- Un componente IBTransaction (pestaña Interbase) llamado Transaccion.

- Dos componentes IBQuery (pestaña Interbase) llamados LstClientes y Clientes.

Se sopone que la base de datos BASEDATOS.FDB esta al lado de nuestro ejecutable. Vamos a comenzar a configurar cada uno de estos componentes.

Hacemos doble clic sobre el componente IBDatabase y en el campo User Name le ponemos SYSDBA. En en password le ponemos masterkey:


Pulsamos OK, y después en la propiedad DefaultTransaction del componente IBDatabase seleccionamos Transaccion, desactivamos la propiedad LoginPrompt y nos aseguramos que en SQLDialect ponga un 3.

Para el componente IBTransaction llamado Transaccion seleccionaremos en su propiedad DefaultDatabase la base de datos BaseDatos.

Como nuestra intención es tener una rejilla que muestre el listado general de clientes y un formulario para dar de alta clientes, lo que vamos a hacer es crear una tabla IBQuery para la rejilla llamada LstClientes y otra tabla para el formulario del cliente llamada Clientes. Sería absurdo utilizar un mismo mantenimiento para ambos casos ya que si tenemos miles de clientes en la rejilla, el tener cargados en memoria todos los campos del cliente podría relentizar el programa.

En los componentes LstClientes y Clientes vamos a configurar también:

Database: BaseDatos
Transaction: Transaccion
UniDirectional: True

El motivo de activar el campo UniDirectional es para que el cursor SQL trabaje más rápido en el servidor ya que los componentes ClientDataSet gestionan en memoria los registros leidos anteriormente.

Como en la rejilla sólo quiero mostrar los campos ID, NOMBRE y NIF entonces en el componente LstClientes en su propiedad SQL vamos a definir:

SELECT ID,NOMBRE,NIF FROM CLIENTES
ORDER BY ID DESC

y para el componente Clientes:

SELECT * FROM CLIENTES
WHERE ID=:ID

En la condición WHERE de la SQL hemos añadido el parámetro :ID para que se pueda más adelante acceder directamente a un registro en concreto a partir de su campo ID.

Una vez definida nuestra capa de acceso a datos en el siguiente artículo nos encargaremos de definir nuestra lógica de negocio.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

26 septiembre 2007

La potencia de los ClientDataSet (I)

El mundo de la programación de bases de datos cambia tan rápidamente que casi no nos da tiempo a asimilar los nuevos protocolos. Comenzamos con DBase, Clipper, FoxPro, .., hasta hoy en día que tenemos Microsoft SQL Server, Interbase, Firebird, etc.

Luego tenemos el maremagnum de lenguajes de programación donde cada cual se come el acceso a datos a su manera: Java con sus infinitos frameswork tales como hibernate, struts, etc., Ruby con el archiconocido Ruby On Rails que utiliza el paradigma MVC (Modelo, Vista, Controlador), Microsoft a su rollo con ADO y su plataforma Microsoft.NET. Todo eso sin contar con los potentes lenguajes script que no tienen la atención que merecen como son PHP, Ruby, Python, TCL/TK, Groovy, etc. Hasta la mismísima CodeGear nos ha sorprendido con su IDE 3rdRails para programación en Ruby On Rails.

Pero si hay algo que hace que Delphi destaque sobre el resto de entornos de programación es su acceso a múltiples motores de bases de datos utilizando una misma lógica de negocio que abstrae al programador de las rutinas a bajo nivel. En la conocida tecnología de acceso a datos llamada MIDAS.

Las primeras versiones de Delphi contaban con el veterano controlador de bases de datos BDE (Borland Database Engine) que permitía acceder a las clásicas bases de datos DBASE, PARADOX, etc. En la versión 5 de Delphi se incluyeron los componentes IBExpres (IBX) que permitían tener un acceso directo a bases de datos Interbase y Firebird. Fue a partir de Delphi 6 cuando Borland apostó por la tecnología DBExpress, un sistema rápido mediante drivers que utilizando un mismo protocolo permitía conectividad con múltiples motores de bases de datos tales como Interbase, Firebird, Oracle, MySQL, Informix, Microsoft SQL Server, etc. Incluso en su última versión (DBX4) permite acceder a su nuevo motor de bases de datos multiplataforma llamado BlackFish programado integramente en .NET y Java (anteriormente se llamaba JDataStore y estaba programado en Java).

Luego tenemos también otros componentes muy buenos de acceso a datos tales como Zeos, Interbase Objects (IBO), FIBPlus, etc. Y por supuesto el conocido protocolo de acceso a datos de Microsoft llamado ADO, siendo su última versión ADO.NET la que tiene bastantes posibilidades de convertirse en un estandar para todos los entornos Windows.

Pero si hay un componente de acceso a bases de datos en Delphi que destaque sobre todos los demás ese es el ClientDataSet. Combina la facilidad de acceso a datos a través de la clase TDataSet y la potencia de controlar automáticamente las transacciones al motor de bases de datos, las SQL de consulta, actualización y eliminación así como la conexión y desconexión de las tablas con el servidor haciendo que el programador no tenga que preocuparse de las particularidades del motor de bases de datos.

El componente de la clase TClientDataSet no conecta directamente sobre una base de datos en concreto, si no que utiliza el componente DataSetProvider que actua de intermediario haciendo de puente entre los componentes de bases de datos (IBX,IBO,etc) y nuestra tabla ClientDataSet. El componente ClientDataSet es algo así como una tabla de memoria (como la que tienen los componentes RX) que se trae y lleva datos a las tablas de la base de datos encargándose automáticamente de las transacciones.

LA ESTRUCTURA CORRECTA DE UN PROGRAMA

Para crear una buena aplicación con acceso a bases de datos hay que dividir nuestro programa en tres partes principales:

Capa de acceso a datos: se encarga de conectar con un motor de bases de datos en concreto ya sea con componentes IBX, ADO, BDE, etc.

Lógica de negocio: aquí se definen como son nuestras tablas (CLIENTES, ARTICULOS,etc), los campos que contienen así como el comportamiento al dar de alta registros, modificarlos, realización de cálculos internos, etc. Todo esto lo haremos con componentes de la clase TClientDataSet y TDataSetProvider.

Interfaz de usuario: se compone de los formularios, informes y menús de opciones que va a visualizar el usuario y que estarán internamente conectados con los ClientDataSet.

La interfaz de usuario sólo podrá acceder a la lógica de negocio y esta última sólo a la capa de acceso a datos. Así, si en un futuro queremos cambiar la interfaz de usuario (por ejemplo para Windows Vista) o el motor de base de datos no afectaría al resto de las capas del programa.

Para ello vamos a utilizar los componentes contenedores de la clase TDataModule para alojar la lógica de negocio y el acceso a datos. En nuestro ejemplo crearemos una base de datos de clientes definiendo las tres capas.

CREANDO LA BASE DE DATOS

En este ejemplo voy a crear una base de datos de clientes utilizando el motor de bases de datos Firebird 2.0 y con los componentes IBX. Las tablas las voy a crear con el programa IBExpert (http://www.ibexpert.com) cuya versión personal es gratis y muy potente. La base de datos se va a llamar BASEDATOS.FDB, pero si haceis las pruebas con Interbase entonces sería BASEDATOS.GDB. No voy a explicar aquí como funciona el programa IBExpert o IBConsole ya que hay en la red información abundate sobre ambos programas.

Creamos la tabla de clientes:

CREATE TABLE CLIENTES (
ID INTEGER NOT NULL,
NOMBRE VARCHAR(100),
NIF VARCHAR(15),
DIRECCION VARCHAR(100),
POBLACION VARCHAR(50),
CP VARCHAR(5),
PROVINCIA VARCHAR(50),
IMPORTEPTE DOUBLE PRECISION,
PRIMARY KEY (ID)
)

Como quiero que el ID sea autonumérico voy a crear un generador:

CREATE GENERATOR IDCLIENTE

Y un disparador para que cuando demos de alta el registro rellene automáticamente el ID y autoincremente el contador del generador:

CREATE TRIGGER CONTADOR_CLIENTES FOR CLIENTES
ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
NEW.ID = GEN_ID( IDCLIENTE, 1 );
END

NOTA IMPORTANTE: Hay algunas versiones de Delphi 7 que tienen un error en los componentes IBExpress (IBX) que hacen que los componentes ClientDataSet no funcionen correctamente. Recomiendo actualizar los componentes IBX a la versión 7.04 que podeis encontrarla en:

http://codecentral.borland.com/Item.aspx?id=18893

En el siguiente artículo comenzaremos a realizar el programa utilizando esta base de datos.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

25 septiembre 2007

La barra de estado

Es raro encontrar una aplicación que no lleve en alguno de sus formularios la barra de estado. Hasta ahora he utilizado ejemplos sencillos de meter texto en la barra de estado de manera normal, pero el componente StatusBar permite meter múltiples paneles dentro de la barra de estado e incluso podemos cambiar el formato de la fuente en cada uno de ellos.

ESCRIBIENDO TEXTO SIMPLE

El componente de la clase TStatusBar tiene dos estados: uno para escribir texto simple en un sólo panel y otro para escribir en múltiples paneles. Supongamos que el componente de la barra de estado dentro de nuestro formulario de llama BarraEstado. Para escribir en un sólo panel se hace de la siguiente manera:

BarraEstado.SimplePanel := True;
BarraEstado.SimpleText := 'Texto de prueba';

Se puede escribir tanto texto como longitud tenga la barra de estado, o mejor dicho, tanto como sea la longitud del formulario.

ESCRIBIENDO TEXTO EN MULTIPLES PANELES

Para escribir en múltiples paneles dentro de la misma barra de estado hay que crear un panel por cada apartado. En este ejemplo voy a crear en la barra de estado tres paneles y en cada uno de ellos voy a poner un formato diferente.

begin
BarraEstado.SimplePanel := False;
BarraEstado.Panels.Clear;

with BarraEstado.Panels.Add do
begin
Text := 'x=10';
Width := 50;
Style := psOwnerDraw;
Alignment := taRightJustify;
end;

with BarraEstado.Panels.Add do
begin
Text := 'y=50';
Width := 50;
Style := psOwnerDraw;
Alignment := taRightJustify;
end;

with BarraEstado.Panels.Add do
begin
Text := 'Texto seleccionado';
Style := psText;
Width := 50;
end;
end;

La propiedad Style de cada panel determina si es psText o psOwnerDraw. Por defecto todos los paneles que se crean tiene el estilo psText (texto normal). Si elegimos el estilo psOwnerDraw significa que vamos a ser nosotros los encargados de dibujar el contenido del mismo. Ello se hace en el evento OnDrawPanel de la barra de estado:

procedure TFormulario.BarraEstadoDrawPanel( StatusBar: TStatusBar; Panel: TStatusPanel; const Rect: TRect );
begin
case Panel.ID of
0: with BarraEstado.Canvas do
begin
Font.Name := 'Tahoma';
Font.Size := 10;
Font.Style := [fsBold];
Font.Color := clNavy;
TextOut( Rect.Left + 2, Rect.Top, Panel.Text );
end;

1: with BarraEstado.Canvas do
begin
Font.Name := 'Tahoma';
Font.Size := 10;
Font.Style := [fsBold];
Font.Color := clRed;
TextOut( Rect.Left + 2, Rect.Top, Panel.Text );
end;
end;
end;

Cuando se van creando paneles dentro de una barra de estado, a cada uno de ellos se le va asignado la propiedad ID a 0, 1, 2, etc, la cual es de sólo lectura. Como puede verse en el evento OnDrawPanel si el ID es 0 lo pinto de azul y si es 1 de rojo. Pero sólo funcionará en aquellos paneles cuyo estilo sea psOwnerDraw. Quedaría de la siguiente manera:


También puede cambiarse en cada panel propiedades tales como el marco (bevel), la alineación del texto (alignment) y el ancho (width).

Esto nos permitirá informar mejor al usuario sobre el comportamiento de nuestra aplicación en tiempo real y de una manera elegante que no interfiere con el contenido del resto del formulario.

Pruebas realizadas en Delphi 7.

24 septiembre 2007

Creando un navegador con el componente WebBrowser

Delphi incorpora dentro de la pestaña Internet el componente de la clase TWebBrowser el cual utiliza el motor de Internet Explorer para añadir un navegador en nuestras aplicaciones.


Seguro que os preguntareis, ¿que utilidad puede tener esto si ya tengo Internet Explorer, Firefox, Opera, etc.? Pues hay ocasiones en que un cliente nos pide que ciertos usuarios sólo puedan entrar a ciertas páginas web. Por ejemplo, si un usuario está en el departamento de compras, es lógico que a donde sólo tiene que entrar es a las páginas de sus proveedores y no a otras para bajarse música MP3 a todo trapo (no os podeis imaginar la pasta que pierden las empresas por este motivo).

Entonces vamos a ver como crear un pequeño navegador con las funcionalidades mínimas.

CREANDO LA VENTANA DE NAVEGACION

Vamos a crear la siguiente ventana:


Incorpora los siguientes componentes:

- Un panel en la parte superior con su propiedad Align fijada a alTop (pegado arriba).

- Dentro del panel tenemos una etiqueta para la dirección.

- Un componente ComboBox llamado URL para la barra de direcciones.

- Tres botones para ir atrás, adelante y para detener.

- Una barra de progreso para mostrar la carga de la página web.

- Un componente WebBrower ocupando el resto del formulario mediante su propiedad Align en alClient, de tal manera que si maximizamos la ventana se respete el posicionamiento de todos los componentes.

CREANDO LAS FUNCIONES DE NAVEGACION

Una vez hecha la ventana pasemos a crear cada uno de los eventos relacionados con la navegación. Comencemos con el evento de pulsar Intro dentro de la barra de direcciones llamada URL:

procedure TFormulario.URLKeyDown( Sender: TObject; var Key: Word; Shift: TShiftState );
begin
if key = VK_RETURN then
begin
WebBrowser.Navigate( URL.Text );
URL.Items.Add( URL.Text );
end;
end;

Si se pulsa la tecla Intro hacemos el objeto WebBrowser navegue a esa dirección. Además añadimos esa dirección a la lista de direcciones URL por la que hemos navegado para guardar un historial de las mismas.

Cuando se pulse el botón atrás en la barra de navegación hacemos lo siguiente:

procedure TFormulario.BAtrasClick( Sender: TObject );
begin
WebBrowser.GoBack;
end;

Lo mismo cuando se pulse el botón adelante:

procedure TFormulario.BAdelanteClick( Sender: TObject );
begin
WebBrowser.GoForward;
end;

Y también si queremos detener la navegación:

procedure TFormulario.BDetenerClick( Sender: TObject );
begin
WebBrowser.Stop;
end;

Hasta aquí tenemos la parte básica de la navegación. Pasemos ahora a controlar el progreso de la navegación así como que el usuario sólo pueda entrar a una página en concreto.

A la barra de progreso situada a la derecha del botón BDetener la llamaremos Progreso y por defecto estará invisible. Sólo la vamos a hacer aparecer cuando comience la navegación. Eso se hace en el evento OnBeforeNavigate2:

procedure TFormulario.WebBrowserBeforeNavigate2( Sender: TObject;
const pDisp: IDispatch; var URL, Flags, TargetFrameName, PostData,
Headers: OleVariant; var Cancel: WordBool );
begin
if Pos( 'terra', URL ) = 0 then
Cancel := True;

Progreso.Show;
end;

Aquí le hemos dicho al evento que antes de navegar si la URL de la página web no contiene la palabra terra que cancele la navegación. Así evitamos que el usuario se distraiga en otras páginas web.

En el caso de que si pueda navegar entonces mostramos la barra de progreso de la carga de la página web. Para controlar el progreso de la navegación se utiliza el evento OnProgressChange:

procedure TFormulario.WebBrowserProgressChange( Sender: TObject; Progress, ProgressMax: Integer );
begin
Progreso.Max := ProgressMax;
Progreso.Position := Progress;
end;

Cuando termine la navegación debemos ocultar de nuevo la barra de progreso. Eso lo hará en el evento OnDocumentComplete:

procedure TFormulario.WebBrowserDocumentComplete( Sender: TObject;
const pDisp: IDispatch; var URL: OleVariant );
begin
Progreso.Hide;
end;

Con esto ya tenemos un sencillo navegador que sólo accede a donde nosotros le digamos. A partir de aquí podemos ampliarle muchas más características tales como memorizar las URL favoritas, permitir visualizar a pantalla completa (FullScreen) e incluso permitir múltiples ventanas de navegación a través de pestañas (con el componente PageControl).

También se pueden bloquear ventanas emergentes utilizando el evento OnNewWindow2 evitando así las asquerosas ventanas de publicidad:

procedure TFormulario.WebBrowserNewWindow2(Sender: TObject;
var ppDisp: IDispatch; var Cancel: WordBool);
begin
Cancel := True;
end;

Aunque últimamente los publicistas muestran la publicidad en capas mediante hojas de estilo en cascada, teniendo que utilizar otros programas más avanzados para eliminar publicidad.

Pruebas realizadas en Delphi 7.

Publicidad