Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Por supuesto, dado que estamos escribiendo manualmente el codigo del com-
ponente y aiiadiendole codigo fuente a1 formulario original, tenemos que recordar
escribir el procedimiento R e g i s t e r .
Una vez escrito el procedimiento R e g i s t e r y preparado el componente, de-
bemos crear un mapa de bits. En 10s componentes no visuales, 10s mapas de bits
son fundamentales ya que no solo se usan en la Component Palette, tambien se
utilizan a1 colocar el componente en un formulario.
Eso es todo lo necesario para ejecutar el cuadro de dialogo que hemos coloca-
do en el componente, como se puede ver en la figura 9.10. Como hemos visto, esta
es una interesante tecnica para el desarrollo de algunos cuadros de dialogo comu-
nes.
Propiedades de coleccion
De vez en cuando, necesitamos propiedades que contengan una lista de valores
en lugar de uno solo. A veces, podemos usar una propiedad basada en
T S t r i n g L i s t , per0 solo es valido con informacion textual (aunque puede aso-
ciarse un objeto a cada string). Cuando necesitemos una propiedad que almacene
una matriz de objetos, la solucion mas adecuada a VCL es usar una coleccion. El
papel de las colecciones es, por diseiio, el de crear propiedades que contengan una
lista de valores. Son ejemplos de propiedades de coleccion en Delphi la propiedad
Colums del componente DBGrid y la propiedad Panels del componente
TStatusBar
one
three
type
TMdMyItem = class (TCollectionItem)
private
FCode: Integer;
FText: string;
procedure SetCode(const Value: Integer);
procedure SetText (const Value: string) ;
published
property Text: string read FText write SetText;
property Code: Integer read FCode write SetCode;
end;
end;
FCollString : = str;
end;
destructor TCanTest.Destroy;
begin
FColl. Free;
inherited;
end:
p r o c e d u r e TMdListPasteAction.ExecuteTarget(Target: TObject);
begin
.
(TargetList ( T a r g e t )) Items .Add (Clipboard.AsText) ;
end;
Una vez escrito este codigo en una unidad y afiadido a un paquete (el paquete
MdPack, en este caso), el paso final consiste en registrar las nuevas acciones
personalizadas en una categoria dada. Esta se indica como el primer parametro
del procedimiento RegisterActions,mientras que el segundo es la lista de
clases de acciones que se van a registrar:
procedure Register;
begin
RegisterActions ( 'List ',
[TMdListCutAction, TMdListCopyAction,
TMdListPasteAction] , nil) ;
end;
Una tecnica mejor seria estraer estos valores del Registro de Windows, donde
todos estos nombres estan almacenados. El metodo Edit es sencillo: crea y muestra
un cuadro de dialogo. Podriamos haber mostrado el cuadro de dialogo Abrir
directamente, per0 hemos decidido afiadir un paso intermedio para permitir a1
usuario probar el sonido. Esto es parecido a lo que Delphi hace con las propieda-
des graficas: primer0 abrimos la vista previa y cargamos el fichero, solo despues
de confirmar que es correcto. El paso mas importante es cargar el fichero y pro-
barlo antes de aplicarlo a la propiedad. Este es el codigo del metodo Edit:
p r o c e d u r e TSoundProperty.Edit;
begin
S o u n d F o r m : = TSoundForm.Create ( A p p l i c a t i o n ) ;
t rY
SoundForm.ComboBoxl.Text : = GetValue;
/ / m o s t r a r e l c u a d r o d e didlogo
i f SoundForm.ShowModal = mrOK t h e n
SetValue (SoundForm.ComboBoxl .Text) ;
finally
SoundForm.Free;
end;
end;
1 IAN shown I
Figura 9.12. La lista de sonidos ofrece una pista al usuario, que tambien puede
escribir el valor de la propiedad o hacer doble clic para activar el editor (mostrado
despues, en la figura 9.13).
d . . 1 IlmIx ~ d l
~p -- - - --
Figura 9.13. El forrnulario del editor de propiedades de sonido muestra una lista de
sonidos disponibles y nos perrnite cargar un fichero y escuchar el sonido
seleccionado.
Los dos primeros botones del formulario tienen un metodo asignado a su even-
to OnClick:
procedure TSoundForm.btnLoadClick(Sender: TObject);
begin
i f 0penDialogl.Execute then
ConboBoxl.Text := 0penDialogl.FileName;
end ;
requires
vcl,
Mdpack ,
designide;
contains
PeSound i n ' PeSound. pas ',
PeFSound i n ' PePSound. pas ' (SoundPorm];
El efecto de este codigo es aiiadir 10s elementos del menu a1 menu local del
formulario, como se ve en la figura 9.14. A1 seleccionar cualquiera de dichos
elementos de menu, simplemente se activa el metodo Executeverb del editor
de componentes:
p r o c e d u r e TMdListCompEditor.ExecuteVerb (Index: Integer);
begin
c a s e Index o f
0: ; // nada que hacer
1: MessageDlg ('Este es un sencillo editor de
componentes '#I3 + 'creado por Marco Cantu1#13 + 'para el libro
"La biblia del Delphi " ' , mtInf ormation, [mbOK] , 0) ;
2: w i t h Component a s TMdListDialog d o
Execute;
end;
end;
Hemos decidido manejar 10s dos primeros elementos en una unica rama de la
sentencia case, a pesar de que podiamos habernos saltado el mensaje de co-
pyright. El otro comando llama al metodo Execute del componente que estamos
editando, determinado usando la propiedad Component de la clase TCompo-
nent E d i t or. Conociendo el tipo del componente, podemos facilmente acceder
a sus metodos despues de una asignacion de tipos dinamica. El ultimo metodo se
refiere a la accion predefinida del componente y se activa haciendo doble clic
sobre el en el Form Designer:
procedure TMdListCompEditor.Edit;
begin
// p r o d u c e un s o n i d o y rnuestra e l c u a d r o A c e r c a d e
Beep;
Executeverb (0);
end:
Figura 9.14. Los elementos del menu personalizado atiadidos por el editor de
propiedades del componente ListDialog.
Hemos aiiadido esta unidad a1 paquete M d D e s P k, que incluye todas las exten-
siones en tiempo de diseiio del capitulo. Despues de instalarla y activar este pa-
quete se puede crear un nuevo proyecto, colocar una componente de lista con
solapa en el y experimentar.
paquetes
El enlace dinamico
En primer lugar, es necesario conocer la diferencia entre el enlace dinamico y
el enlace estatico a funciones o procedimientos. Cuando una subrutina no esta
disponible directamente en un archivo fuente, el compilador aiiade la subrutina a
una tabla de simbolos interna. El compilador, debe haber visto, por supuesto, la
declaracion de la subrutina y conocer sus parametros y tip0 o producira un error.
Tras la compilacion de una subrutina normal (estatica), el editor de enlaces
obtiene el codigo compilado de la subrutina desde una unidad compilada de Delphi
(o biblioteca estatica) y lo aiiade a1 codigo del programa. El archivo ejecutable
resultante incluye todo el codigo del programa y de las unidades relacionadas. El
editor de enlaces de Delphi es lo suficientemente inteligente como para incluir la
cantidad minima de codigo de las unidades utilizadas por el programa y enlazar
so10 las funciones y mktodos que en realidad se utilizan.
.
mano 3 .
la flexittilidad obtenida rnediante las funciones virtuales y el reducido ta-
- - ejecurames
ae 10sarcmvos d .- .
que se - '-
L . X . -
conslgue I.
Ilmltanao -
el uso ae esas .r 3 1 .
funciones virtuales.
En el caso del enlace dinamico, que tiene lugar cuando nuestro codigo llama a
una funcion basada en una DLL, el editor de enlaces sencillamente utiliza la
informacion de la declaracion externa de la subrutina para instalar algunas tablas
en el archivo ejecutable. Cuando Windows carga el archivo ejecutable en memo-
ria, carga primer0 todas las DLL necesarias y, a continuacion, arranca el progra-
ma. Durante este proceso de carga, Windows rellena las tablas internas del
programa con las direcciones de las funciones de las DLL en memoria. Si por
alguna razon no se encuentra la DLL o una rutina referenciada no esta en una
DLL encontrada, el programa ni siquiera arranca.
Cada vez que el programa llama a una funcion externa, utiliza esta tabla inter-
na para reenviar la llamada a1 codigo de la DLL (que ahora esta situado en el
espacio de direcciones del programa). Fijese en que en esta estructura no hay dos
aplicaciones diferentes. La DLL se transforma en parte del programa en ejecucion
y se carga en el mismo espacio de direcciones. Todo el paso de parametros tiene
lugar en la pila de la aplicacion (porque la DLL no tiene una pila aparte) o en 10s
registros del procesador. Dado que una DLL se carga en cl espacio de direcciones
de la aplicacion, cualquier asignacion de memoria de la DLL o cualquier informa-
cion global que esta crea, reside cn el espacio de direcciones del proceso princi-
pal. Por ello, pueden pasarse informacion y punteros a memoria directamente
entre la DLL y el programa. Esto puede extenderse a1 paso de referencias a obje-
tos, lo que puede resultar problematico porque el ejecutable y la DLL pueden
tener una clase compilada diferente (para solucionar esto se pueden utilizar 10s
paquetes, como se vera posteriormente en este capitulo).
Existe otra tecnica de uso de las DLL aun mas dinamica que la que acabamos
dc mcncionar. Dc hccho, cn ticmpo dc cjccucion, podemos cargar una DLL en
memoria, buscar una funcion (siempre que sepamos su nombre) y llamar a la
funcion por su nombre. Esta tecnica requiere un codigo mas complejo y emplea
mas tiempo en localizar la funcion. Sin embargo, la ejecucion de la funcion posee
la misma velocidad que la llamada de una DLL cargada de forma implicita. Por el
contrario, no es necesario que la DLL este disponible a1 arrancar el programa.
Usaremos este enfoque mas adelante en el ejemplo DynaCall.
Otra interesante ventaja es que podemos ofrecer una version diferente de una
DLL. que sustituya a la actual. Si las subrutinas de la DLL tienen 10s mismos
parametros, podemos ejecutar el programa con la nueva version de la DLL sin
tener que volver a compilarlo. No importa en absoluto que la DLL tenga subrutinas
nucvas. Solo puede haber problemas si falta una rutina de la version antigua de la
DLL cn la nueva. Tambien puede haber dificultades si la nueva DLL no implementa
las funciones de una forma compatible con el funcionamiento de la antigua DLL.
Esta segunda ventaja es aplicable particularmente a aplicaciones complejas.
En caso de tener un programa muy grande que requiera actualizaciones y correc-
ciones frecuentes, dividirlo en multiples archivos ejecutables y bibliotecas dina-
micas nos permitira distribuir unicamente las partes modificadas en lugar de un
solo ejecutable de gran tamaiio. Hacer esto es especialmente importante con las
bibliotecas del sistema de Windows: generalmente, no es necesario recompilar
nuestro codigo si Microsoft publica una version actualizada de las bibliotecas del
sistema de Windows (en una nueva version del sistema operativo o en un paquete
de actualizacion).
Otra tecnica habitual es usar las bibliotecas dinamicas solo para almacenar
recursos. Podemos crear diferentes versiones de una DLL que contenga cadenas
de texto para diferentes idiomas y asi cambiar el idioma en tiempo de ejecucion, o
preparar una biblioteca de iconos e imagenes y utilizarlos en diferentes aplicacio-
nes. El desarrollo de versiones de un programa adaptadas a varios idiomas es muy
importante y Delphi incluye soporte para esto mediante su entorno integrado de
traduccion, el Integrated 7'ranslntion Envrronment (ITE).
Otra ventaja clave es que las DLL son independientes del lenguaje de progra-
macion. La mayoria de 10s entornos de programacion Windows, asi como la ma-
yoria de 10s lenguajes de macro de aplicaciones para usuarios finales, permiten a1
programador llamar a una funcion almacenada en una DLL. Esta flexibilidad se
aplica solo para el uso de funciones. Para compartir ob-jetos en una DLL entre
lengua-jes de programacion, deberiamos cambiar a la infraestructura COM o a la
arquitectura .NET.
.
www .d e l p h i - j edi org.
Las tres funciones realizan algunos calculos basicos sobre 10s parametros y
devuelven el resultado. Fijese en que todas las funciones se definen con el modifi-
cador W I NAP I,que define la convencion de llamada a parametros adecuada y
van precedidas de la declaracion de c 1spec ( dl lexport ) , que hace que
las funciones estkn disponibles paraprogramas externos.
Dos de estas funciones C++ utilizan tambien la convencion de nombrado de C
(indicada por la sentencia extern " c " ) ,per0 la tercera, Add,no. Esto afecta
a1 mod0 en que llamamos a estas funciones desde Delphi. De hecho, 10s nombres
internos de las tres funciones corresponden a sus nombres en el archivo de codigo
fuente en C++, a excepcion de la funcion ~ d dDado . que no hemos utilizado la
clausula extern "c" para esta funcion, el compilador C++ ha utilizado la
tecnica name mangling o de manipulacion de nombres. Se trata de una tecnica
utilizada para incluir informacion sobre el numero y tip0 de parametros en el
nombre de la funcion, que necesita el lenguaje C++ para implementar la sobrecar-
ga de funciones. El resultado a1 usar el compilador Borland C++ es un nombre de
funcion muy raro: @Add$qqsii.En realidad, este es el nombre que tenemos que
usar en nuestro ejemplo en Delphi para llamar a la funcion Add de la DLL (lo
cual explica por que tenemos que evitar normalmente la tecnica name mangling
de C++ en las funciones exportadas y por que las declaramos generalmente como
extern " c " ) .Lo que sigue son declaraciones de las tres funciones en el ejem-
plo Delphi CallCpp:
function Add (A, B: Integer) : Integer;
stdcall; external ' CPPDLL. DLL' name ' @ ~ d d $ q q s i i' ;
function Double (N: Integer) : Integer;
stdcall ; external ' CPPDLL . DLL ' name ' Double ' ;
function Triple (N: Integer) : Integer;
stdcall; external ' C P P D L L . D L L ' ;
Como podemos ver, se puede exponer u omitir el alias para la funcion externa.
Hemos ofrecido uno para la primera funcion (no habia otra alternativa, porque el
nombre de la funcion DLL exportada @Add$qqsii no es un identificador Delphi
valido) y para la segunda, aunque en el segundo caso no sea necesario. De hecho,
si 10s dos nombres se corresponden, podemos omitir la directiva name, como en
el caso de la tercera funcion anterior. Si no estamos seguros de 10s nombres reales
de las funciones exportadas por la DLL, podemos usar el programa de linea de
comandos TDump de Borland, disponible en la carpeta Delphi BIN, con el
parametro -ee.
Hay que recordar aiiadir la directiva s t d c a l l a cada definicion, por lo que el
modulo de llamada (la aplicacion) y el modulo que se va a llamar (la DLL) usan
la misma convencion para pasar parametros. De no hacerlo asi, obtendremos
valores aleatorios pasados como parametros, un error que es muy dificil rastrear.
Para usar esta DLL en C++, hemos creado un ejemplo en Delphi, denominado
CallCpp. Su sencillo formulario tiene botones para llamar a funciones de la DLL
y algunos componentes visuales para parametros de entrada y salida (vease la
figura 10.1). Fijese en que para ejecutar esta aplicacion, deberiamos tener la DLL
en el mismo directorio que el proyecto, en uno de 10s directorios de la ruta o en 10s
directorios Windows o System. Si movemos el archivo ejecutable a un nuevo
directorio e intentamos ejecutarlo, obtendremos un error en tiempo de ejecucion
indicando que no existe dicha DLL:
Figura 10.1. La salida del ejemplo CallCpp al hacer clic en cada uno de 10s botones.
Como ya hemos dicho, es util construir una DLL cuando hay una parte del
codigo del programa sometida a frecuentes cambios. En este caso, podemos susti-
tuir frecuentemente la DLL y mantener el resto del programa igual. Asi, cuando
es necesario escribir un programa que ofrezca distintas funciones para distintos
grupos de usuarios, podemos distribuir versiones diferentes de una DLL a dichos
usuarios.
exports
Triple, Double;
exports
Triple (N: Integer),
Triple (C: Char) name ' T r i p l e c h a r ' ;
- - - -- - - - - - -
Esta segunda version es mas compleja, per0 la primera solo se puede usar
desde Delphi. Ademas, en la primera version es necesario que incluyamos la
unidad ShareMem y que distribuyamos el archivo BorlndMM.DLL como ya se ha
explicado anteriormente.
Esta declaracion es similar a las usadas para llamar a la DLL en C++. Esta
vez, en cambio, no hay problemas con 10s nombres de las funciones. Despues de
haber vuelto a declararlas como e x t e r n a l , las funciones de la DLL se pueden
usar como si fueran funciones locales. Veamos un ejemplo, con llamadas a las
funciones relacionadas con cadenas (la figura 10.2 muestra un ejemplo del resul-
tado) :
p r o c e d u r e TForml.BtnDoubleStringClick(Sender: TObject);
begin
// l l a m a a l a f u n c i o n d e l a DLL d i r e c t a m e n t e
EditDouble.Text : = Doublestring (EditSource.Text, I ; ' ) ;
end ;
Figura 10.2. El resultado del ejemplo CallFrst, que llama a la DLL que hernos creado
en Delphi.
{SLIBSUFFIX 60 ' 1
Figura 10.3. La pggina Application del cuadro de dialogo Project Options tiene ahora
una seccion llamada Library Name.
cons t
DllName = ' F i r s t d l l . d l l ' ;
Lo que hace que este codigo sea diferente del codigo que escribimos normal-
mente en un programa es el uso del control de excepciones:
Un bloque t r y / e x c e p t protege a toda la funcion, de mod0 que se atra-
para cualquier excepcion creada por la funcion, mostrando un mensaje
adecuado. La razon para controlar toda excepcion posible es que la aplica-
cion que realiza la llamada podria estar escrita en cualquier lenguaje, so-
bre todo en uno que no sepa como controlar excepciones. Aun cuando el
que llama es un programa en Delphi, a veces, resulta util la misma tecnica
de proteccion.
Un bloque t r y / fi n a l l y protege las operaciones realizadas en el for-
mulario, asegurando que el formulario se destruira correctamente, aunque
se produzca una excepcion.
A1 comprobar el valor de retorno del metodo ShowModal, el programa esta-
blece el resultado de la funcion. Hemos definido el valor predefinido antes de
introducir el bloque t r y para garantizar que siempre se va a ejecutar (y evitar
tambien la advertencia del compilador que indica que el resultado de la funcion
podria no estar definida).
Podemos encontrar este codigo en 10s proyectos FormDLL y UseCol de la
carpeta FormDLL. (Tambien encontraremos el archivo WORDCALL. TXT que
muestra como llamar a la rutina desde una macro de Word). El ejemplo muestra
tambien que podemos aiiadir un formulario no modal a la DLL, per0 esto causa
muchos problemas. El formulario no modal y el principal no se sincronizan ya
que la DLL tiene su propio objeto A p p l i c a t i o n global en su propia copia de
la VCL. Este problema puede ser parcialmente solventado copiando el H a n d l e
del o b j e t o A p p 1 i c a t i o n del programa a1 H a n d l e del o b j e t o A p p 1 i c a t i o n
de la biblioteca. No todos 10s problemas se resuelven con el codigo del ejemplo.
Una solucion mejor puede ser compilar el programa y la biblioteca para usar
paquetes Delphi, de mod0 que el codigo VCL y la inforrnacion no se dupliquen.
Pero esta tecnica causa aun algunos problemas: generalmente esta recomendado
no usar las DLL y 10s paquetes de Delphi juntos. La mejor tecnica para hacer que
10s formularios de una biblioteca sean accesibles para otros programas Delphi es
usar paquetes en lugar de las DLL.
N m e . _ .-- . ~ - B a s s ! W - - P h -
1
OLEAUT32 dl $770F0000 C \WINDOWS\syslem32\oI
rnsvul dl1 $77BE0000 C\WlNDOWS\syslern32\M
de32 dl1 $771800W C \WINDOWS\syslern3nO
VERSION dll $77BD0000 C \WINDOWS\sydern32\v
CDMCTL32 dY 977310000 C.\WINDOWS\syslem32\c
bhmndl -W m W - - D:\md7de\lO\DMm\~
MSCTF dl $74680000 C \WINDOWS\syslern32\M
TabHook dR $10000WO C \WINDOWS\Syslern32\I
UxTherne dl1 $58150000 C \WINDOWS\Syslern32\u
MOUSEDLL dl1 $00780000 C W~chwosde progma\Br
Es importante saber que la direccion base de una DLL es algo que podemos
solicitar activando la opcion de direccion base. En Delphi esta direccion se deter-
mina mediante el valor Image Base de la pagina del editor de enlaces del cuadro
de dialog0 Project Options. En la biblioteca DllMem, por ejemplo, esta puesta
en $ 0 0 8 0 0 0 0 0 . Necesitamos tener un valor diferente para cada una de nuestras
bibliotecas, asegurandonos de que no entra en conflict0 con ninguna biblioteca
del sistema u otra biblioteca (paquete, control ActiveX y otros) usada por el
ejecutable. Esto tambien puede verificarse mediante la ventana Module del depu-
rador .
A pesar de que esto no garantiza una ubicacion unica, fijar una direccion base
para una biblioteca es siempre mejor que no hacerlo; en este caso siempre ocurre
una reubicacion, pero la probabilidad de que dos ejecutables diferentes reubiquen
la misma biblioteca en la misma direccion no es muy alta.
uses
SysUtils,
DllMemU i n ' DllMemU. p a s ' ;
exports
S e t D a t a , GetData,
GetShareData, S e t S h a r e D a t a ;
end.
p r o c e d u r e SetShareData ( I: Integer) ; s t d c a l l ;
begin
ShareData" : = I ;
end;
cons t
VirtualFileName = ' S h a r e D 1 l D a t a l ;
DataSize = sizeof (Integer);
initialization
// crea un archivo proyectado e n memoria
hMapFile : = CreateFileMapping (SFFFFFFFF, nil,
Page-Readwrite, 0, DataSize, VirtualFileName);
if hMapFile = 0 then
raise E x c e p t i o n - C r e a t e ('Error creando archivo proyectado
e n memoria ' ) ;
Si hacemos clic sobre el segundo boton, el programa copia 10s datos de la DLL
en el segundo cuadro de edicion:
Edit2 .Text := IntToStr (GetData);
El tercer boton se usa para mostrar las direcciones de memoria de una funcion,
con el codigo fuente que aparece a1 principio de este apartado. Los ultimos dos
botones tienen basicamente el mismo codigo que 10s dos primeros, per0 llaman a1
procedimiento Set ShareData y a la funcion Get ShareData. Si ejecutamos
dos copias de este programa, podemos ver que cada copia tiene su propio valor
para 10s datos globales normales de la DLL, mientras 10s valores de 10s datos
compartidos son comunes. Hay que definir valores diferentes en 10s dos progra-
mas y despues obtener ambos para ver el efecto. Esta situacion se muestra en la
figura 10.4.
Figura 10.4. Si ejecutarnos dos copias del prograrna UseMern, verernos que 10s
datos globales de su DLL no son cornpartidos.
Versiones de paquetes
Un elemento muy importante y normalmente incomprendido es la distribucion
de paquetes actualizados. Cuando actualizamos una DLL, podemos incluir la
nueva version y 10s programas ejecutables que necesiten dicha DLL todavia fun-
cionaran (a menos que hayamos eliminado las funciones exportadas previamente
existentes o cambiado algunos de sus parametros).
Sin embargo, cuando distribuimos un paquete Delphi, si actualizamos el pa-
quete y modificamos la parte de interfaz de cualquier unidad del paquete, podria
ser necesario compilar de nuevo todas las aplicaciones que usen el paquete. Esto
es necesario si aiiadimos metodos o propiedades a una clase, per0 no si aiiadimos
nuevos simbolos globales (o modificamos algo no utilizado por aplicaciones de
cliente). Con respecto a 10s cambios que afecten solo a la seccion de implernentacion
de la unidad de paquete no existe problema alguno.
Un archivo DCU en Delphi posee un indicador de version basado en su sello de
informacion sobre la ultima actualizacion y en una suma de verificacion, calcula-
dos desde la parte de interfaz de la unidad. Cuando cambiamos la parte de interfaz
de una unidad, todas las demas unidades basadas en ella deberian compilarse de
nuevo. El compilador compara el sello de informacion sobre la ultima actualiza-
cion y la suma de verificacion de la unidad de compilaciones previas con el nuevo
sello de informacion sobre la ultima actualizacion y la nueva suma de verificacion
y decide si la unidad dependiente ha de ser compilada de nuevo. Esa es la razon
por la que debemos compilar de nuevo cada unidad cuando conseguimos una
nueva version de Delphi que ha modificado unidades de sistema.
En Delphi 3 (cuando se introdujeron por primera vez 10s paquetes), se aiiadio
una suma de verificacion del paquete, como funcion de entrada adicional a la
biblioteca de paquetes, obtenida a partir de la suma de verificacion de las unida-
des contenidas y la suma de verificacion de 10s paquetes necesitados. Esta suma
de verificacion era posteriormente llamada por 10s programas que usaban el pa-
quete de mod0 que cualquier ejecutable basado en una version antigua no arran-
caria. Delphi 4 y las versiones siguientes hasta Delphi 7 han disminuido las
restricciones en tiempo de ejecucion del paquete. Sin embargo, las restricciones
en tiempo de diseiio en archivos DCU siguen siendo identicas. La suma de verifi-
cation de 10s paquetes ya no se comprueba, por lo que podemos modificar direc-
tamente las unidades que forman parte de un paquete y desplegar una nueva
version del paquete que se va a usar con el archivo ejecutable existente. Dado que
nos referimos a 10s metodos por nombre, no podemos eliminar ningun metodo
existente. No podemos ni siquiera cambiar sus parametros, debido a las tecnicas
name mangling aiiadidas especificamente a 10s paquetes para protegerlos contra
cambios en sus parametros.
Eliminar un metodo referenciado desde un programa lo detendra durante el
proceso de carga. Si hacemos otros cambios el programa puede fallar inesperada-
mente durante la ejecucion. Por ejemplo, si sustituimos un componente de un
formulario compilado en un paquete por un componente similar, el programa que
hace la llamada puede ser capaz aun de acceder a1 componente en esa posicion de
memoria, aunque ahora sea diferente. Si decidimos cambiar la interfaz de las
unidades de un paquete sin recompilar todos 10s programas que las usan, deberia-
mos limitar 10s cambios. A1 aiiadir propiedades nuevas o metodos no virtuales a1
formulario, deberiamos ser capaces de mantener la compatibilidad total con 10s
programas que ya usan el paquete. Ademas, aiiadir campos y metodos virtuales
puede afectar a la estructura interna de la clase, derivando en problemas con
programas existentes, que esperan una informacion de clase y un formato de
tablas de metodos virtuales (VMT) diferentes.
ADVERTENCIA: Esto se refiere la distribuci6n en programas cornpila-
dos divididos en EXE y paquetes, no a la distribuci6n dr: componentes a
otros desarrolladores en Delphi. En este ultimo caso, las normas sobre
versiones son m b estrictas y deberiamos prestar una atencibn especial a
las versiones de 10s paquetes.
Una de las ventajas de esta tecnica esta en que podemos referirnos a un formu-
lario compilado en un paquete con un codigo esactamente igual a1 que usaremos
para un formulario compilado en el programa. En realidad, si compilamos este
programa, la unidad del formulario se enlazara a el. Para guardarlo en el paquete,
tenemos que usar paquetes en tiempo de ejecucion para la aplicacion y aiiadir de
forma manual el paquete PackWithForm a la lista de paquetes en tiempo de ejecu-
cion (esto no lo sugiere el IDE de Delphi porque no hemos instalado el paquete en
el entorno de desarrollo).
Despues de esto, compilamos el programa y se comportara como es habitual.
Pero ahora el formulario esta en un paquete DLL e incluso se puede modificar el
formulario en el paquete, compilarlo de nuevo y ejecutar sencillamente la aplica-
cion para ver 10s efectos. Sin embargo, fijese en que con la mayor parte de 10s
cambios que afectan a la parte de interfaz de las unidades del paquete, deberiamos
compilar tambien de nuevo el programa ejecutable que llama a1 paquete.
proyectos son todos referenciados por el archivo & grupo de proyecfo (BPG)
de la carpeta.
Para resumir estos cambios, veamos el codigo utilizado por el programa prin-
cipal (la aplicacion DynaPackForm) para mostrar el formulario modal desde el
paquete cargado dinamicamente:
procedure TForml.BtnChangeClick(Sender: TObject);
var
FormScroll: TForm;
FormClass: TFormClass;
HandlePack: HModule;
begin
// intenta cargar e l paquete
HandlePack := LoadPackage ( ' PackWi thForm. bpl ) ;
if HandlePack > 0 then
begin
FormClass := TFormClass (Getclass ( ' TFormScroll' ) ) ;
if Assigned (FormClass) then
begin
FormScroll : = FormClass .Create (Application);
try
// inicia 10s datos
SetPropValue (FormScroll, ' SelectedColor' , Color) ;
// muestra el formulario
if FormScroll.ShowModal = mrOK then
Color : = GetPropValue (FormScroll, lSelectedColor');
finally
FormScroll.Free;
end;
end
else
ShowMessage ('Form class not found');
UnloadPackage (Handlepack);
end
else
ShowMessage ( I Package not found1);
end;
Cabe destacar que el programa descarga el paquete tan pronto como ha acaba-
do con el. Este paso no es obligatorio. Podiamos haber movido la llamada
U n l o a d P a c k a g e a1 manejador O n D e s t r o y del formulario para asi evitar la
recarga del paquete despues de la primera Ilamada.
Ahora podemos ejecutar el programa sin que el paquete este disponible y vere-
mos que arranca de forma adecuada, solo indicara que no puede encontrar el
paquete cuando pulsemos el boton Change. En este programa no es necesario
utilizar paquetes en tiempo de ejecucion para mantener la unidad fuera del archi-
vo ejecutable porque no se referencia a la unidad desde el codigo. Ademas, no es
necesario que el paquete PackWithForm este en la lista de paquetes en tiempo de
ejecucion.
De todos modos, debemos utilizar paquetes en tiempo de ejecucion o el progra-
ma incluira las variables globales VCL (como el objeto A p p l i c a t i o n ) y el
paquete cargado de forma dinamica contendra otra version, porque se referira de
todos modos a 10s paquetes VCL.
unit IntfColSel:
interface
uses
Graphics, Contnrs;
type
IColorSelect = interface
[ ' {3F96l395-7lF6-48ZZ-BDO2-3B475PF516D4} ' ]
function Display (Modal: Boolean = True) : Boolean;
procedure SetSelColor (Col: TColor);
function GetSelColor: TColor;
property SelColor: TColor
read GetSelColor write SetSelColor;
end;
var
ClassesColorSelect: TClassList;
implementation
procedure RegisterColorSelect (AClass: TClass);
begin
i f ClassesColorSelect. IndexOf (AClass) < 0 then
ClassesColorSelect.Add (AClass);
end;
initialization
ClassesColorSelect := ~~1assList.Create;
finalization
ClassesColorSelect.Free;
end.
Estructura de un paquete
Podriamos preguntarnos si es posible saber si una unidad ha sido enlazada en
el fichero ejecutable o si forma parte de un paquete en tiempo de ejecucion. No
solo esto es posible en Delphi, sino que incluso se puede explorar la estructura
general de una aplicacion. Un componente puede utilizar la variable global
Module I s Pac kage, declarada en la unidad SysInit. No deberiamos necesitar
esta variable, per0 es tecnicamente posible tener un componente con codigos dife-
rentes dependiendo de si esta empaquetado o no. El siguiente codigo extrae el
paquete en tiempo de ejecucion que alberga a1 componente, si lo hubiera:
var
fPackName: string;
begin
// o b t i e n e e l n o m b r e d e l p a q u e t e
SetLength (fPackName, 100);
i f ModuleIsPackage then
begin
GetModuleFileName (HInstance, PChar (fPackName), Length
(fP a c k N a m e ) ) ;
fPackName : = PChar (fPackName) // a j u s t a l a l o n g i t u d d e
l a cadena
end
else
fPackName : = ' N o t p a c k a g e d ' ;
Ademas de acceder a la informacion sobre el paquete desde el interior del
componente (corno en el codigo anterior), tambien podemos hacer lo mismo desde
un punto de entrada especial de las bibliotecas de paquetes, la funcion
G e t P a c k a g e I n f o T a b l e . Esta devuelve cierta informacion especifica sobre
el paquete que Delphi guarda como recursos e incluye en el paquete DLL. Afortu-
nadamente, no necesitamos tecnicas de bajo nivel para acceder a esta informa-
cion, porque Delphi ofrece algunas funciones de alto nivel para manipularlo.
Podemos usar dos funciones para acceder a la informacion sobre el paquete:
GetPackageDescription: Devuelve una cadena que contiene una descrip-
cion del paquete. Para llamarla, tenemos que dar el nombre del modulo (la
biblioteca de paquetes) como unico parametro.
GetPackageInfo: No devuelve directamente informacion sobre el paquete,
sino que hay que pasarle una funcion a la que llamar para cada entrada de
la estructura de datos interna del paquete. En la practica, G e t P a c k a -
g e 1n f o llamara a nuestra funcion para cada unidad contenida en el pa-
quete. Ademas, G e t P a c k a g e I n f o define varios indicadores en una
variable I n t e g e r .
Estas dos llamadas a funciones nos permiten acceder a informacion interna
sobre un paquete, per0 para saber que paquetes esta usando nuestra aplicacion,
podemos mirar el archivo ejecutable que utilizan las funciones de bajo nivel. Sin
embargo, Delphi nos proporciona un metodo mas sencillo. La funcion
EnumModules no devuelve directamente informacion sobre 10s modulos de la
aplicacion, sino que nos permite pasarle una funcion, a la que llama para cada
modulo de la aplicacion, el archivo ejecutable principal y para cada uno de 10s
paquetes que necesita la aplicacion.
Para demostrar esta tecnica, hemos construido un sencillo ejemplo que mues-
tra la informacion sobre paquetes y modulos en un componente TreeView. Cada
nodo del primer nivel corresponde a un modulo y dentro de cada modulo hemos
creado un subarbol que muestra 10s paquetes que contiene y necesarios para dicho
modulo, asi como la descripcion del paquete y 10s indicadores de compilacion
(RunOnly y D e s i g n O n l y ) . Podemos ver el resultado de este ejemplo en la
figura 10.6.
Ademas del componente TreeView, hemos aiiadido otros componentes a1 for-
mulario principal, per0 10s hemos ocultado: un DBEdit, un Chart y un
FilterComboBox. Hemos aiiadido estos componentes para incluir mas paquetes
en tiempo de ejecucion en la aplicacion, ademas de 10s ubicuos paquetes VCL y
RTL. El unico metodo de la clase de formularios es F o r m C r e a t e , que llama a
la funcion de enumeracion del modulo:
procedure TForml. FormCreate (Sender: TObject) ;
begin
EnumModules (ForEachModule, nil) ;
end;
D Contams
Packlnfo [ M a ~ nUnd )
PackFo~m
Sysln~t
Requ~res
n C.\WINDOWS\System3Z\vcldb70 bpl
.: Contalns
Syslnrl
VDQConsta
DBOleCll
DBCtrls
DBLogDlg
DBPclns
dbcgr~ds
DBGrlds
+ Flequurs
Deicr~p(~on Borland Database Components
Fun Drily
- C \~dINDOWS\Syslem32\dbrII70 bpl
+ Conta~ns
+ Rcqu~res
Descnpt~or~ Borland Core Dslabase Components
Run Only
- r \ \ A I I N ~ ~ \ A I < \ < , , * I P ~M~~\I~-~~
Figura 10.6. El resultado del ejernplo Packlnfo, con la lista de paquetes que usa.
/ / o b t i e n e l a description y a d a d e n o d o s a j u s t a d o s
ModuleDesc : = GetPackageDescription (PChar (ModuleName));
ContNode : = AddChild (ModuleNode, ' C o n t a i n s ' ) ;
ReqNode : = AddChild (ModuleNode, ' R e q u i r e s ' ) ;
Modelado y UML
UML (Unified Modeling Language) es una noticacion grafica usada para ex-
presar el analisis y diseiio de un proyecto software y comunicarselo a otras perso-
nas. UML es independiente del lenguaje, per0 esta pensado para la descripcion de
proyectos orientados a objetos. Como resaltan 10s creadores de UML, no se trata
de una metodologia, y puede usarse como una herramienta descriptiva sin impor-
tar cual sea el proceso de diseiio preferido.
Vamos a fijarnos en 10s diagramas UML desde el punto de vista de un progra-
mador de Delphi que use ModelMaker.
Diagramas de clase
Uno de 10s diagramas UML mas comunes soportados por ModelMaker es el
diagrama de clase. Los diagramas de clase pueden mostrar una amplia variedad
de relaciones de clase, pero, en su minima expresion, este tipo de diagrama mues-
tra un conjunto de clases u objetos y las relaciones estaticas existentes entre ellos.
Por ejemplo, la figura 1 1.1 muestra un diagrama de clase que contiene las clases
del programa NewDate de un capitulo anterior. Si 10s resultados son distintos
cuando se importen estas clases en ModelMaker y se Cree un diagrama de clase
propio; hay que tener en cuenta las numerosas opciones comentadas anteriormen-
te. Muchos parametros controlan el mod0 en que se muestran las clases
visualizadas. Se puede abrir el archivo de ModelMaker (MPB) usado para la
figura 1 1.1 desde la carpeta de codigo fuente del capitulo actual.
- IDBle. TDdeTne;
+ Day: Inleger;
.
atlrlbr~CS
Mo* Inleger.
Yaar M*p:
0Wnions
- GetDay IMeger; Assign(. )
- G e t ~ o m hhleger; Denease(..)
Getyear, Imeger, + GelTeut. d r h g
- SefDay( .) +Increase(..)
- SelManlh(..) + Leapyea. BncFa~nc~a.
- SelYear( ) SelVW.I
+ Gesde r Setvalue( )
+Create( )
Diagramas de secuencia
Los diagramas de secuencia modelan la interaccion entre objetos representan-
do 10s objetos y 10s mensajes transmitidos entre ellos a lo largo del tiempo. En un
diagrama de secuencia tipico, 10s objetos que interactuan dentro de un sistema se
organizan a lo largo del eje X y el tiempo se representa desde arriba hacia abajo,
a lo largo del eje Y. Los mensajes se representan como flechas entre objetos. Se
puede ver un ejemplo de un diagrama de secuencia bastante trivial en la figura
1 1.4. Los diagramas de secuencia pueden crearse a diversos niveles de abstrac-
cion, con lo que se permite representar la interaccion del sistema a alto nivel,
con solo unos pocos mensajes, o una interaccion a bajo nivel, con muchos men-
3
- t h i i lrxepn
IJumpei
-- TAlh!.et~
Tkhlele
= SetPosp'alue: Inlegerj;
--
TAlhkle
= GelPor Inleper; TAlhkle
J w *;le
3
- w a :rcr*
Podlan-InNpe,
Redundanl rnembar TAthlele
+ C~ede. TAIhlete Leave m!undanl rnrrbs
+ Deshy. TAlhlele Leave ~ A n d a nml d m f
+ IJumplrnpl TJurnperlrnpl. TAthlele Leave ledurdanl memba
+ Jurpcl TJurnperlrnpl TAtHele Leave rcdmdanl member
+ Wdkl: dnng: TAlhlcte Leave l e d d a n t mcmbcr
Junto con 10s diagramas de clase, 10s diagramas de secuencia son unos de 10s
diagramas UML soportados por ModelMaker que se relacionan mas de cerca con
el modelo de codigo.
Se pueden crear varios diagramas en ModelMaker en 10s cuales 10s simbolos
usados no tienen una relacion directa con el modelo de codigo, per0 en 10s diagramas
de secuencia se puede afectar directamente a1 codigo de las clases del modelo y a
sus metodos. Por ejemplo, cuando se crea un simbolo para un mensaje entre
objetos. se puede escoger de entre una lista de metodos que corresponden a1 objeto
receptor, o se puede decidir afiadir un metodo nuevo a1 objeto, y el nuevo metodo
se aiiadira a1 modelo de codigo. Fijese en que tal y como se comento, ModelMaker
no creara automaticamente diagramas de secuencia para el codigo importado;
sera necesario crearlos de forma manual.
-13 s 7 u +i k ru
+ Oeatcly rn b lntepn)
a PissglSwce 7 0 4 4
* 3 Dec~eeselNumbetOlDayr
, 3 Ge(Tex( simg
+
+, Increas$4mbnOiD~
* Leapyea Bookan
1
, iu a !a( ChFck h Insert Nmw~-nct-~.m
Figura 11.4. Un d~agramade secuencla para un controlador de eventos del ejemplo
NewDate.
Diagramas no UML
ModelMaker soporta tres diagramas que no son estandar de UML, per0 son
bastante utiles:
Diagramas de mapa mental (Mind-Map): Creados por Tony Buzan en la
decada de 1960. Un metodo excelente para tormentas de ideas, explorar
temas con ramificaciones o registrar rapidamente pensamientos encadena-
dos. Suelen usarse para mostrar datos genericos durante presentaciones.
Diagramas de dependencia de unidades: Suelen usarse para mostrar 10s
resultados del potente Unit Dependency Analyzer de ModelMaker. Estos
diagramas pueden mostrar relaciones ramificadas de unidades dentro de un
proyecto Delphi.
Diagramas de robustez: Estos diagramas se han dejado fuera de la espe-
cificacion UML, tal vez sin mucho sentido. Ayudan a salvar el obstaculo
entre el modelado de casos de uso de solo interfaces y la implementacion de
diagramas de secuencia especificos. Los diagramas de robustez pueden
ayudar a un equipo de analisis a verificar sus casos de uso y orientarse
hacia 10s detalles de la implementacion.
Elementos comunes de 10s diagramas
Cada tip0 de diagrama soportado por ModelMaker contiene simbolos especifi-
cos para ese tipo de diagrama, per0 existen varios elementos en el Diagram Editor
que son comunes a todos 10s tipos de diagrama. Se pueden aiiadir imagenes y
formas a 10s diagramas, a1 igual que simbolos de paquetes (contenedores a 10s que
se pueden aiiadir otros simbolos).
El simbolo de hiperenlace permite aiiadir a un diagrama una etiqueta enlazada
con alguna otra entidad. De hecho, la amplia mayoria de 10s simbolos de diagra-
ma soportan esta caracteristica de hiperenlace. Se puede enlazar a otro diagrama
(hacer clic sobre el enlace abrira el diagrama enlazado en el editor), se puede
enlazar a un elemento dentro del modelo de codigo (una clase, unidad, metodo,
interfaz,. . ., de manera que a1 hacer clic sobre el enlace se abra el editor apropiado
para el elemento enlazado) o se puede enlazar a un documento externo (este enla-
ce abrira el documento enlazado con la aplicacion apropiada).
Hay disponibles tres tipos distintos de herramientas de anotacion para cada
tip0 de diagrama. Varios documentos explican con mas detalle estas herramien-
tas, baste pues decir aqui que se puede aiiadir un simbolo de anotacion indepen-
diente, uno que muestre automaticamente la documentacion interna del objeto
enlazado o aiiadir un simbolo de anotacion, escribir texto en el y enlazar este
simbolo a un objeto. Cuando se hace esto, la documentacion interna del objeto se
actualiza para que se corresponda con el texto del simbolo de anotacion.
Los simbolos de relacion o asociacion son de manera predeterminada lineas
rectas. Sin embargo, se puede convertir en lineas ortogonales seleccionando el
simbolo y pulsando Control-0. Tambien se puede aiiadir notas a estos simbolos
pulsando Control y haciendo clic sobre la linea. ModelMaker intenta mantener
estos simbolos ortogonales siempre que sea posible.
Ahora, ModelMaker ofrece un robusto conjunto de estilos de simbolos visua-
les. Se pueden definir estilos de fuente y color de un mod0 jerarquico y aplicarlos
por nombre a 10s simbolos del diagrama. Para conseguir mas informacion, es
aconsejable buscar "style manager" en la ayuda electronica.
Una ultima caracteristica habitual a resaltar es la capacidad de ordenar
jerarquicamente la lista de diagramas (vease figura 11.5). Se pueden aiiadir car-
petas de cara a la organizacion y reordenar 10s diagramas. Para ello basta con
mantener pulsada la tecla Control y arrastrar un diagrama a su nuevo diagrama
padre.
Las prestaciones de diagramas de ModelMaker ofrecen una amplia gama de
posibilidades; una vez que se haya dedicado algo de tiempo a comprender el
conjunto de prestaciones, seguramente se descubra que el proceso de desarrollo se
transforma. Para el desarrollador de Delphi, la naturaleza activa bidireccional del
Diagram Editor ofrece una experiencia de diagramacion mucho mas dinamica que
la mayoria de 10s editores UML que simplemente generan imagenes estaticas bien
acabadas.
M!;$
,T SUML
almplemenlabonD~agram
Collaboral~onDlagram
m ~ l a s D~aglam
s
Use Case D~agram
.Yr I.^ ,
8s -cut Wizard. .
La mayoria de las opciones de menu solo se encuentran disponibles si
ModelMaker esta en funcionamiento. Una vez que se haya arrancado ModelMaker
(desde la opcion de menu Run ModelMaker o de un mod0 normal), el resto de
10s elementos quedara disponible. El menu de integracion contiene unos cuantos
modos de aiiadir codigo a un modelo. Los elementos Add to Model,Add Files
to Model, Convert to Model y Convert Project to Model hacen
que ModelMaker importe las unidades especificadas: 10s elementos Add impor-
tan unidades en el modelo cargado actualmente en ModelMaker, y 10s elementos
Convert crean un modelo nuevo e importan las unidades.
Convert Project to Model es un buen sitio para comenzar (nos asegurare-
mos de hacer una copia del codigo, y despues seleccionaremos este elemento del
menu mientras que esta abierto uno de 10s proyectos en Delphi). El proyecto a1
completo se importara a un nuevo modelo en ModelMaker.
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Dates, StdCtrls;
type
MMW1N:CLASSINTERFACE TDateForm; ID=37;
var
DateForm: TDateForm;
implementation
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Dates, StdCtrls;
implementation
La vista de diferencias
Como ya se comento, es facil caer en una situacion en que el modelo y 10s
archivos fuente pierdan su sincronizacion. Si se han editado tanto el modelo como
10s archivos fuente, no se puede simpleinente volver a generar 10s archivos a
partir del modelo, porque se sobrescribirian 10s cambios hecho en 10s archivos.
Del mismo modo, no se pueden volver a importar las unidades, pues seguramente
se eliminarian 10s cambios del modelo. Por suerte. ModelMaker ofrece una de las
herramientas de analisis de diferencias mas robustas que esisten. Cuando el mo-
delo haya perdido la sincronia, es hora de usar la pestaiia Difference (vease la
figura 1 1.8).
> laddE.lnkrr 6 - u a d
1
- - --
141 I Frncrtr and API Praicd
- -- ---- -
f
- 3.
4.7 * + , . C 1
SelC!dsrlnrcrurmnledaOos~IMM A
I
.b M A o a 3 n g B&a
Id 7 FModeLoadmu Bodeen
+. 1( ModePatComl lntw
7 FModtParICoun( I n l w ,
Id 9 GsrModelParlCwr lnteper,
+ .A MyNmPrqmty In(w
1 FMyPlswPrope~lyIntegef
GelMyNewPmpefly Inlepn
SetM:NcwPapetcomt aV& I
*. .a PrcgfersVaClc I n t q i w'
>
I . L-c 7 PLS 1 Irmt --
Figura 11.7. ModelMaker con la pestaiia de Irnplernentacion activa.
--
I - I p7t<~_--- I I - - I 1,-
Figura 11.8. La vista Difference de ModelMaker.
Documentacion y macros
ModelMaker puede ser muy practico para soportar 10s esfuerzos de documen-
tacion de software. Necesitaremos dominar un concept0 muy importante antes de
continuar (aunque no es muy complejo): dentro de ModelMaker, la documenta-
cion no equivale a 10s comentarios. No hay que tener miedo; se pueden hacer
cosas complicadas con 10s comentarios del codigo fuente, per0 hay que dar algu-
nos pasos para que ModelMaker emita (o importe) esos comentarios. Casi cual-
quier elemento del modelo (clases, unidades, miembros, simbolos de diagrama y
demas) pueden tener documentacion, per0 introducir documentacion para un ele-
mento no provocara que la documentacion aparezca automaticamente en el codi-
go fuente. El texto estara vinculado a1 elemento dentro del modelo, per0 hay que
provocar que ModelMaker genere un comentario en el codigo fuente que contenga
esa documentacion.
TmyClass.DoSome thing
Returns: String
Visibility: Public
C o m e nt:
Este es el comentario real que queremos que importe
ModelMaker. Las cinco primeras lineas d e este bloque de
comentario no deberian importarse a 1 modelo. )
Reingenieria de codigo
Reingenieria es uno de esos terminos llamativos de la programacion de 10s que
constantemente se habla, per0 que significa cosas distintas para gente distinta. La
reingenieria es basicamente el proceso de mejorar el codigo existente in situ sin
alterar su comportamiento externo. No existe un unico proceso de reingenieria a1
que haya que acogerse, es simplemente el trabajo de tratar de mejorar el codigo in
situ sin provocar una gran perturbacion.
Muchos textos se dedican a este concepto, asi que simplemente prestaremos
atencion a las formas que tiene ModelMaker de ayudar a reorganizar el codigo.
Una vez mas, el modelo de codigo interno de ModelMaker tiene un papel impor-
tante (recordemos que desarrollar en ModelMaker no es simplemente desarrollar
codigo orientado a objetos, tambien es un proceso de desarrollo que es asistido
por la orientacion a objetos). Ya que todos estos elementos del modelo de codigo
se guardan internamente como objetos (objetos que tienen referencias cruzadas) y
ya que las unidades de codigo fuente se vuelven a generar completamente a partir
de este modelo cada vez que se decide generar el codigo, cualquier cambio en 10s
elementos del codigo se propagara a traves de las clases instantaneamente.
El ejemplo perfecto vuelve a ser una propiedad de clase. Si se tiene una propie-
dad llamada MyNewProperty con metodos de lectura y escritura (mantenidos
por ModelMaker y llamados G e t M y N e w P r o p e r t y y S e t M y N e w P r o p e r t y )
y se quiere renombrar la propiedad como M y p r o p e r t y , solo es necesario un
paso: renombrar la propiedad. ModelMaker se encargara del resto (10s metodos
de acceso se renombraran automaticamente como G e t M y P r o p e r t y y
S e t M y P r o p e r t y ) . Si la propiedad aparece en un diagrama, el diagrama se
actualizara automaticamente para representar el cambio. (Una advertencia:
ModelMaker no buscara automaticamente instancias deMyNewPropert y en el
codigo, habra que realizar una busqueda y sustitucion global dentro de
ModelMaker. Se trata de un ejemplo sencillo, per0 muestra el mod0 en que
ModelMaker simplifica las labores de reingenieria, como mover y renombrar ele-
mentos de codigo, donde ModelMaker se encargara de la mayoria de 10s detalles
en lugar del desarrollador. Estos son algunos casos especificos:
Renombrado sencillo: Esta tarea es bastante simple y ya la hemos comen-
tado, per0 su utilidad nunca se resaltara lo suficiente. Los cambios en el
nombre de un elemento del modelo de codigo se propagaran gracias a
ModelMaker a traves del modelo de codigo a todas las instancias de ese
elemento que conozca.
Reasignacion de clases: Este proceso absurdamente simple puede reali-
zarse de unas cuantas maneras distintas. Lo mas habitual es simplemente
arrastrar una clase en la vista Classes desde un nodo padre a otro (tam-
bien se puede hacer en un diagrama de clase arrastrando la flecha de gene-
ralizacion desde el antecesor anterior hasta el nuevo), y las clase tendra un
nuevo padre. Si la herencia esta restringida, ModelMaker actualizara
automaticamente 10s metodos heredados de la clase hija para que se corres-
pondan con las declaraciones de la nueva clase padre. La siguiente vez que
se genere el codigo, estos cambios apareceran automaticamente.
Mover clases entre unidades: Tambien es una labor sencilla. En la vista
Units, se arrastra la clase desde su lugar actual a la nueva unidad. Todo el
codigo relevante (declaraciones, implementaciones y comentarios) se vol-
vera a generar en la nueva unidad.
Mover miembros entre clases: En la reingenieria, este proceso se conoce
como "desplazamiento de prestaciones (o responsabilidades) entre obje-
tos". La idea es sencilla: a medida que el desarrollo progresa, se puede
descubrir que es mas apropiado mover ciertas responsabilidades
(implementadas como miembros de clase) a otra clase. Se puede hacer
mediante arrastrar y soltar. Seleccionamos 10s miembros de clase deseados
de entre las lista de miembros y 10s arrastramos sobre la nueva clase en la
vista Classes (manteniendo pulsada la tecla Mayus para mover en lugar
de copiar).
Conversi6n de miembros: Se trata de una de las prestaciones de
reingenieria de ModelMaker mas practicas. Hacemos clic con el boton
derecho del raton sobre un miembro en la Member List para que aparezca
el menu contextual que contiene las opciones y subopciones de menu
Convert To. Seleccionando una de estas subopciones podremos convertir
un miembro de clase ya existente de un tip0 de miembro a otro. Por ejem-
plo, si tenemos un campo privado llamado FMyInteger y queremos
convertirlo a un propiedad, ModelMaker crea automaticamente una pro-
piedad publica llamada My1 nteger, que lee desde y escribe en
FMyInteger.Del mismo modo, se puede convertir este campo en un
metodo (sera una funcion privada Ilamada MyInteger que devolvera un
entero).
Herencia restringida: En el cuadro de dialog0 del editor de metodos hay
una casilla de verificacion Inheritance Restricted. Cuando se inarca
esta casilla, ModelMaker no permite modificar la mayoria de 10s atributos
del metodo, ya que esos atributos se determinan de acuerdo con la
implernentacion del metodo sobrescrito en la clase antecesora. Si se cam-
bia la declaracion de un metodo en una clase antecesora, esos cambios se
aplicaran automaticamente a cualquier clase descendiente donde se haya
restringido la herencia del metodo que sobrescribe.
Si se tiene experiencia con la reingenieria (o se han usado las ultimas versiones
de JBuilder). esto puede no parecer un conjunto de herramientas de reingenieria
particularmente impresionante. Sin embargo, cuando se compara con lo que es
posible hacer solo en Delphi, se trata de un increible conjunto de posibilidades.
Ademas, la API OpenTools de ModelMaker ofrece acceso a la mayor parte del
modelo de codigo. Si no se esta contento con lo que ModelMaker ofrece tal y
como se instala, se pueden ampliar sus capacidades por uno mismo.
NOTA: Ademas, podemos decir que hay versiones beta (en el momento de
la elaboracion de este libro) de una version fbtura de ModelMaker que time
nuevas herramientas de reingenieria. La mayoria de ellas surgen &I libro
de Martin Fowler sobre la reingenieria, y son impresionantes.
Aplicacion de patrones de diseiio
ModelMaker se esfuerza a1 maximo en su soporte de patrones de diseiio.
ModelMaker proporciona la comodidad de aplicar una implementacion patron
con un simple clic de raton. Segun el patron escogido, puede tener lugar una gran
variedad de acciones. Algunos patrones muestran un asistente antes de aiiadir
codigo y otros simplemente aiiadirhn sus miembros a la clase seleccionada. Como
ya se comento, estos miembros nuevos pertenecen a ModelMaker y como resulta-
do es facil actualizarlos. Ademas, si se decide eliminar la aplicacion del patron,
ModelMaker eliminara cualquier miembro de clase que haya aiiadido para ese
patron. Como ejemplo, podemos fijarnos en el patron Singleton. Supongarnos que
tenemos una clase y solo queremos que como mucho exista una instancia de ella.
Esta es la clase de muestra:
tYPe
TOneTimeData = class (TObject)
private
FGlobalCount: Integer;
procedure SetGlobalCount(const Value: Integer);
public
property GlobalCount : Integer read FGlobalCount write
SetGlobalCount;
end ;
Patrones de disefio
Mientras que 10s programadores se concentran en la irnplementacion de
clases especificas, 10s diseiiadores se centran m b en conseguir que clases y
objetos distintos hncionen juntos. Aunque es dificil ofrecer una definici6n
n r e ~ i c sAn1 A i c n i i ~AP c n f k w a r ~W
yIWWIUCL U W I U a o W Y V Y W UVLCVIUIW,
mm m c ~ m ~C a t r s t a AP
iP
Y W D W I I W I U UV % & U C U U W
11 nrrran;va&An rle
I U V16LUILOWfiVY U W A U
11
Plantillas de codigo
Otra potente prestacion mas en ModelMaker (que parece perdida, escondida
entre miles de otras caracteristicas) son las plantillas de codigo, una tecnica que
se puede usar para crear implementaciones propias de patrones de diseiio. Las
plantillas de codigo son como una diapositiva de parte de una clase que puede
aplicarse a otra clase. En otras palabras, se trata de un conjunto de miembros de
clase, guardados en una plantilla que puede aiiadirse a otra clase, de golpe. Aun
mejor, estas plantillas pueden parametrizarse (corno las macros) para que cuando
se apliquen a una clase aparezca un cuadro de dialogo solicitando rellenar ciertos
valores, que despues se aplican como parte de la plantilla. Un ejemplo es una
propiedad matriz. Declarar una propiedad matriz es sencillo en ModelMaker,
per0 implementar completamente una requiere varios pasos: no solo se necesita la
propiedad matriz en si, si no tambien una T L i s t o clase descendiente que con-
tenga 10s elementos de la matriz y un sistema para proporcionar un recuento de
10s elementos almacenados. Incluso para este ejemplo tan sencillo, se requiere
algo de trabajo para conseguir que la propiedad de matriz quede lista y en funcio-
namiento. Aqui entra en accion la plantilla de la propiedad matriz. Abrimos un
modelo en ModelMaker (o creamos un nuevo modelo y aiiadimos un descendiente
de T O b j e c t ) y escogemos una clase a la que queramos aiiadir la nueva propie-
dad matriz. Hacemos clic con el boton derecho sobre la Member List y seleccio-
narnos Code Templates. Ahora deberia aparecer una barra de herramientas
flotante llamada Code Templates (fijese en que se trata de la misma barra de
herramientas disponible en la pestaiia Patterns). Hacemos clic sobre el boton
Apply Array Property Template para abrir el cuadro de dialogo Code Template
Parameters. Contiene una lista de elementos que se puede especificar para la
plantilla que se va a aplicar, como muestra la figura 11.10. Se puede resaltar
cualquier elemento de la columna de la izquierda y pulsar la tecla F2 para editar
el valor de ese parametro. Aceptamos 10s valores predeterminados y hacemos clic
sobre OK. La clase deberia contener ahora 10s siguientes miembros:
private
FItems : TList;
protected
function GetItemCount: Integer;
function GetItems (Index: Integer) : TObject;
public
property Itemcount: Integer read GetItemCount;
property Items [Index: Integer] : TObject read GetItems;
Para crcar una plantilla de codigo propia, comenzaremos con una clase exis-
tente que ya tenga 10s miembros que se desean convertir en una plantilla. Selec-
cionamos esa clase y, a continuacion, en la Member List, seleccionamos 10s
micmbros que deseamos usar (puede tratarse de cualquier tip0 de miembro). Ha-
cemos clic con el boton derecho sobre la Member List y seleccionamos Create
Code Template. Aparecera el cuadro de dialogo Save Code Template. Es
muy parccido a1 cuadro de dialogo Guardar como estandar (y se especifica
donde guardar la plantilla), per0 se puede especificar el mod0 en que se desea que
aparezca la plantilla. Especificamos un nombre para la plantilla y la pagina de la
paleta dc plantillas en que se desea que aparezca. Prestamos atencion a1 mensaje
de confirmacion resultante; se puede modificar la imagen de la paleta si se desea.
Ahora la nueva plantilla estara disponible en la paleta de plantillas, de manera
que se pucde aiiadir esta plantilla a cualquier clase. Para parametrizar la planti-
lla, hay quc modificar el archivo PAS que se creo cuando se guard6 la plantilla.
Por ejemplo, esto es parte del archivo A r r a y P r o p -List .p a s usado por la
plantilla Array Property:
unit ArrayProp-List;
/ / D E F I N E M A C R O :I t e m s = n a m e o f a r r a y p r o p e r t y
//DEFINEMACRO: TObject=type of a r r a y p r o p e r t y
//DEFINEMACRO:IternCount=Method r e t u r n i n g # i t e m s
//DEFINEMACRO:FItems=TList F i e l d s t o r i n g items
Cuando se aiiade esta propiedad a una clase como parte de la plantilla, las
macros (cosas como < ! ~te m s ! >) seran sustituidas por el valor del parametro
apropiado. De este modo, se pueden usar parametros para personalizar en profun-
didad las plantillas de codigo.
Implernentacion de IUnknow
Es importante que revisemos en primer lugar algunos conceptos basicos de
COM. Todo objeto COM debe implementar la interfaz IUnknown, tambien deno-
minada llnterface en Delphi, para usar interfaces que no sean de tipo COM. Esta
es la interfaz base de la que heredan todas las interfaces de Delphi, y Delphi
proporciona un par de clases diferentes con implementaciones de IUnknownl
I I n t e r f a c e listas para utilizar, como T I n t e r f a c e d O b j e c t y
TComOb j e c t . La primera sc puede utilizar para crear un objeto interno no
relacionado con COM, mientras que la segunda se utiliza para crear objetos que
pueden ser exportados por servidores. Como ya se vera, existen varias clases mas
que heredad de TComOb j e c t y proporcionan soporte para mas interfaces, que
son requeridas por servidores Automation o controles ActiveX.
La interfaz IUnknown tiene tres metodos: AddRe f , Re l e a s e y
Q u e r y 1n t e r f a c e . Aqui e s t i la definicibn de lainterfaz l ~ n k n o w n
(extraida
de la unidad System):
type
IUnknown = i n t e r ace
['{OOOOOOOO-0000-0000-COOO-000000000046}']
f u n c t i o n Q u e r y I n t e r f a c e ( c o n s t I I D : TGUID;
out O b j ) : Integer; stdcall;
f u n c t i o n -AddRef: I n t e g e r ; s t d c a l l ;
f u n c t i o n -Release: I n t e g e r ; s t d c a l l ;
end;
Figura 12.1. Un ejemplo de 10s GUID generados por el ejemplo NewGuid. Los valores
dependen del ordenador en el que se ejecute el programa y del momento de
ejecucion.
uses
ComServ;
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
begin
end.
Las cuatro funciones que exporta la DLL son necesarias para la compatibili-
dad COM y el sistema las usa de la manera siguiente:
Para acceder a la biblioteca de clases (Dl 1 G e t C l a s sob j e c t ) .
Para comprobar si el servidor ha destruido todos sus objetos y se puede
descargar de la memoria (Dl ICanUnloadNow).
Para aiiadir o eliminar informacion sobre el servidor en el Registro de
Windows ( D l l R e g i s t e r S e r v e r y D l l U n r e g i s t e r S e r v e r ) .
Normalmente, no tendremos que implementar estas funciones, porque Delphi
nos ofrece una implementacion predefinida en la unidad c o m s e r v . Por esta ra-
zon, en el codigo de nuestro servidor, solo necesitamos exportarlas.
I
Figura 12.2. El asistente COM Object Wizard.
. I
el oojeto. .
fJnica: Indica que, incluso cuando varias aplicaciones cliente necesitan
. LVM, SOIO.a
existe I . . . ae la ap~icac~on
. una unica insrancia
. I .. - , servi-
.
codigo. Estos son 10s puntos clave de 10s diversos modelos de hilos:
Modelo unico: No se trata de un soporte real para hilos. Las solicitudes
I1 1. LUIW
que llegan a1 servlaor1 n .-I' I I.
se serializan para que el clienre pueaa
- - L I
/ / crea e l segundo o b j e t o
Num2 : = CreateComObject (Class-Number) as INumber;
Label2. Caption : = ' Num2: ' + IntToStr (Num2.Getvalue) ;
Button3.Enabled : = True;
Button4.Enabled : = True;
end ;
Automatizacion
Hasta ahora hemos visto que se puede utilizar COM para permitir que un
archivo ejecutable y una biblioteca compartan objetos. Sin embargo, la mayoria
de las veces, 10s usuarios quieren aplicaciones que se comuniquen entre si. Uno de
10s enfoques que se pueden utilizar para obtener este objetivo es la Automatizacion
(Azrtomation, antes llamada Automatizacion OLE u OLE Azrtomatlon). A conti-
nuacion comentaremos el desarrollo de controladores Autornatizacion para Word
y Excel, mostrando como transferir inforrnacion de bases de datos a estas aplica-
ciones.
-- .- ---
NOTA: La documentacibn actual de Microsoft usa el termino Automati-
zacibn en lugar de Automatizacion OLE y usa 10s timinos documento
activo y documento compuesto en lugar de Documento OLE. Este libro
utiliza la nueva tenninologia, aunque la antigua tenninologia "OLE" sigue
estando indicada y probablemente resulte mhs clara.
En Windows, las aplicaciones no viven en mundos aparte, sino que 10s usua-
rios suelen querer que estas interactuen entre si, a1 igual que 10s usuarios pueden
copiar y pegar datos entre aplicaciones. Sin embargo, cada vez hay mas progra-
mas que ofrecen una interfaz de Automatizacion para que otros programas la
dirijan.
Mas alla de la gran ventaja de la automatizacion programada, en comparacion
con operaciones manuales del usuario, estas interfaces son completamente neu-
trales en cuanto a1 lenguaje, por lo que se puede usar Delphi, C++, Visual Basic
o un lenguaje de macros para controlar un servidor de Automatizacion, indepen-
dientemente del lenguaje de programacion usado para escribirlo. La Automatizacion
tiene una implernentacion sencilla en Delphi gracias a la labor extendida del
compilador y la VCL para proteger a 10s desarrolladores de su complejidad. Para
soportar Automatizacion, Delphi proporciona un asistente y un potente editor de
bibliotecas de tipos y soporta interfaces dobles. Cuando se usa una DLL en pro-
ceso, la aplicacion cliente puede usar el servidor y llamar directamente a sus
metodos, porque estan en el mismo espacio de direccion. Cuando se usa
Automatizacion, la situacion es mas compleja. El cliente (llamado controlador) y
el servidor, son dos aplicaciones separadas que funcionan en distintos espacios de
direccion. Por esta razon, el sistema debe enviar las llamadas a metodos usando
un complejo mecanismo de paso de parametros llamado marshalling
(intermediacion).
Tecnicamente, soportar Automatizacion en COM implica implementar la
interfaz IDispatch, declarada en Delphi dentro de la unidad System como:
type
IDispatch = interface (IUnknown)
['{00020400-0000-0000-COOO-000000000046)"
function GetTypeInfoCount(out Count: Integer): HResult;
s tdcall ;
function GetTypeInfo(Index, LocaleID: Integer;
o u t TypeInfo) : HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
Namecount, LocaleID: Integer; DispIDs: Pointer):
HResult; stdcall;
function Invoke (DispID: Integer; const IID: TGUID;
LocaleID: Integer; Flags: Word; var Params;
VarResult, ExceptInfo, ArgError: Pointer): HResult;
s tdcall ;
end;
El primer0 de 10s dos metodos devuelve la informacion de tipo; 10s dos ultimos
pueden usarse para invocar un metodo real del servidor de Automatizacion. En
realidad, la invocacion solo la realiza el ultimo metodo, I n v o k e , mientras que
G e t I D s O f N a m e s se usa para determinar el identificador de invocacion (nece-
sario para I n v o k e ) a partir del nombre del metodo. Cuando se crea un servidor
de Automatizacion en Delphi, todo lo que se tiene que hacer es definir una biblio-
teca de tipos e implementar su interfaz. Delphi proporciona el resto de lo necesa-
rio mediante su compilador y el codigo de la VCL (en realidad una parte de la
VCL llamada originalmente marco de trabajo DAX).
El papel de IDispath resulta mas patente cuando se considera que hay tres
maneras de que un controlador llame a 10s metodos expuestos por un servidor de
Automatizacion:
Puede solicitar la ejecucion un metodo, pasando su nombre en una cadena,
de forma similar a la llamada dinamica a una DLL. Esto es lo que hace
Delphi cuando s e usa una variante para llamar a1 servidor de
Automatizacion. Esta tecnica es muy sencilla de usar, per0 es bastante
lenta y no ofrece una gran verificacion de tipos del compilador. Implica
una llamada a G e t I D s O f Names seguida de otra a I n v o k e .
Puede importar la definition de una interfaz de Delphi de invocacion
( d i s p i n t e r f a c e ) para el objeto en el servidor y llamar a sus mCtodos
de forma mas directa (simplemente enviando un numero, es decir, llaman-
do directamente a I n v o k e ya quc el DispID de cada metodo se conoce
en tiempo de compilacion). Esta tkcnica, basada en interfaces, permite a1
compilador comprobar 10s tipos de 10s parametros y produce un codigo
mas rapido, per0 requiere un poco mas de esfuerzo por parte del programa-
dor (el uso de una biblioteca de tipos). Ademas. terminamos vinculando el
controlador de la aplicacion a una version especifica del servidor.
Puede llamar directamente a la interfaz, mediante la tabla virtual de la
interfaz, tratandola por ejemplo como un objeto COM normal. Esto fun-
ciona en la mayoria de 10s casos ya que la mayor parte de las interfaces de
10s servidores de Automatizacion ofrecen interfaces duales (que soportan
tanto IDispatc h como una interfaz COM simple).
_
-.-
N O T---.
.. .--.-
--
A r Sa niteAe
- .- _
r..--- iicnr lrna a
-
"
.. --
....-vnrinnte
. .----- --- -- ..--
..
nnrn oi~nrAnr
--a a - - -- -
-
- *
NOTA: Como s e vera mas adelante, las ultimas versiones de Word siguen
registrando la interfaz Word. Basic, que se corresponde con el lenguaje
interno de macros WordBasic, pero tambikn registran la nueva interfaz
Word .Application,que se corresponde con el Ienguaje de macros VBA.
Delphi ofrece componentes que simplifican la conexibn con las aplicacio-
nes de Microsoft Offke.
Estas tres lineas de codigo arrancan Word (a no ser que ya se este ejecutando),
crean un nucvo documento y aiiaden algo de texto. Puede verse el resultado de
esta aplicacion en la figura 12.3. Lamentablemente, el compilador de Delphi no
tiene ningun mod0 de comprobar si existen 10s metodos. Realizar toda la compro-
bacion de tipos en tiempo de ejecucion es algo arriesgado, ya que si se comete el
mas minimo error ortografico en el nombre de una funcion no se vera ningtin
aviso sobre el error hasta que se ejecute el programa y se llegue a esa linea de
codigo. Por ejemplo, si se hubiera escrito VarW. Isnert,el compilador no se
quejaria del error, pero en tiernpo de ejecucion ese error saltaria, ya que no reco-
noce el nombre, Word asume que el metodo no existe.
Aunque la interfaz IDispatch soporta el enfoque que se acaba de ver, tambien
es posible (y mas seguro) que un servidor exporte la descripcion de sus interfaces
y objetos mediante una biblioteca de tipos. Esta biblioteca de tipos puede enton-
ces convertirse mediante una herramienta especifica (como Delphi) en definicio-
nes escritas en el lenguaje que se quiere usar para escribir el programa cliente o
controlador (como el lenguaje Delphi). Esto posibilita que un cornpilador corn-
pruebe que el codigo es correct0 y poder usar las caracteristicas Code Completion
y Code Parameters en el editor de Delphi.
Figura 12.3. El documento d e Word creado mediante la aplicacion WordTest d e
Delphi.
Una vez que el compilador ha realizado sus pruebas, puede usar una de las dos
tecnicas distintas para enviar la pcticion a1 servidor. Puede usar una simple
V T a b l e (es decir, una entrada en una declaracion de tipo de interfaz) o usar una
d i s - p i n t e r f a c e (una interfaz de envio o dispatch).
Ya se ha utilizado una declaracion de tip0 de interfaz, asi que deberia resultar
familiar. Una d i s p i n t e r f ace es basicamente un mod0 de proyectar cada
entrada de un interfaz sobre un numero.
Las llamadas a1 servidor pueden enviarse entonces mediante llamadas de nu-
meros a I D i s p a t c h .I n v o k e , sin el paso adicional de llamar a I D i s p a t c h .
G e t I D s O f N a m e s . Se puede considerar que esta es una tecnica intermedia, a
medio camino entre enviar el nombre de la funcion y usar una llamada directa de
la V T a b l e
Figura 12.4. El editor de la biblioteca de tipos con 10s detalles de una interfaz.
Hay dos recomendaciones que conviene seguir para trabajar mejor con el edi-
tor de biblioteca de tipos de Delphi. La primera y mas sencilla es que si se hace
clic con el boton derecho del raton sobre la barra de herramientas y se activa la
opcion Text Labels se vera en cada boton de la barra un texto comentando su
efecto, lo que simplificara el uso del editor.
La segunda y m b importante es acudir a la pagina Type Library del cuadro
de dialog0 Environment Options de Delphi y activar el boton de radio de len-
guaje Pascal en lugar del lenguaje IDL. Esta configuration determina la notacion
usada por el editor de biblioteca de tipos para mostrar 10s metodos y parametros,
e incluso para editar 10s tipos de 10s parametros de un metodo o el tip0 de una
propiedad.
A no ser que se este acostumbrado a escribir codigo COM en C o C++, proba-
blemente se prefiera pensar en tkrminos de Delphi que en terminos de IDL.
ADVERTENCIA:En esta parte del libro comentaremos cdmo interactuar
con el editor de la biblioteca de tipos cuando se dispone de esta c o d g u r a -
cion, ya que proporciona tambiCn una descripcidn en tirminos de IDL seria
tanto mas confuso como complejo, sin necesidad alguna de ello.
A continuation, esta dispint erf ace, que asocia un numero con cada
elemento de la interfaz IFirs t Server:
type
IFirstServerDisp = dispinterface
['{89855B42-8EFE-11D0-98D0-444553540000]']
procedure Changecolor; dispid 1;
property Value: Integer dispid 2;
end;
La ultima parte del archivo incluye una clase creadora, que se utiliza para
crear un objeto en el servidor (y por ello se usa en la parte cliente de la aplicacion,
no el servidor):
type
CoFirstServer = class
class function Create: IFirstServer;
class function CreateRemote (const MachineName: string) :
IFirstServer;
end:
Delphi ya nos ofrece el esquema del codigo de 10s metodos, por lo que sola-
mente tenemos que completar las lineas intermedias. En este caso, 10s tres meto-
dos se refieren a una propiedad y dos metodos que hemos aiiadido a1 formulario.
En general, no deberiamos aiiadir un codigo relativo a la interfaz de usuario
dentro de la clase del objeto servidor. Nosotros lo hemos hecho asi porque quere-
mos cambiar la propiedad V a l u e y obtener un efecto colateral (mostrar el valor
en un cuadro de edicion). Este es el formulario en tiempo de disefio:
initialization
TAutoObjectFactory.Create(ComServer, TFirstServer,
Class-Firstserver, ciMultiInstance);
end.
- - -.-.7,7 - .- .
--7 7 -- - -
NOTA: En este caso hemos selecciooadouna instanciacion y a p l e . .
names: TF~rstServer
I
Ud 6 name: I~.~rchwo
desprograma\8orIand\D~h17\Impo .. I I
Despues puede usarse para llamar a 10s metodos como es habitual, despues de
haber asignado un objeto a la variable, convirtiendo el objeto devuelto por la
clase creadora:
DMyServer : = CoFirstServer.Create as IFirstServerDisp;
A menos que el servidor ya se encuentre activo, se crea una copia del programa
y se modifica el color, pero entonces se cierra el servidor inmediatamente porque
el objeto de tip0 interfaz sale de su alcance. El metodo alternativo que hemos
utilizado en el ejemplo TlibCli es declarar el objeto como un campo del formu-
lario y crear el objeto COM al arrancar, como en este procedimiento:
procedure TClientForm. Formcreate (Sender: TObject) ;
begin
I M y S e r v e r : = CoFirstServer.Create;
end ;
NOTA: Se inicia una variante como el tipo var Empty cuando se crea. Si
en cambio se asignara el valor nulo a la variante, su tip0 se convertiria en
varNull. Ambos tipos representan variantes sin valor asignado, per0 se
comportan de un mod0 diferente al evaluar la expresion. El valor varNull
.. , . .. . . .. ..
slempre se propaga a una expreslon (convlrtlendola en una expreslon nula),
mientras que el valor varEmpty desaparece sin hacerse notar.
El sewidor en un componente
A1 crear un programa cliente para este servidor u otro servidor de
Automatizacion, se puede utilizar una tecnica mejor: envolver el servidor COM
en un componente Delphi. Si se obsenla la parte final del archivo TlibdemoLib
TLB se v e r i la +declaracibn de una clase T Firstserver que hereda de
TOleServer.Se trata de un componente generado cuando se importa la biblio-
teca, que cl sistema registra en el procedimiento Register de la unidad.
Si se aAade esta unidad a un paquete, el nuevo componente servidor estara
disponible en la Component Palette de Delphi (en la pagina ActiveX, de mane-
ra predefinida). La generacion del codigo de este componente esta controlada por
una casilla de verification que se encuentra en la parte inferior del cuadro de
dialog0 Import Type Library, que mostraba la figura 12.5.
Se ha creado un nuevo paquete, PackAuto, que se encuentra disponible en un
directorio con el mismo nombre. En este paquete se ha aiiadido la directiva
LIVE SERVER-AT-DESIGN TIME en la pagina Directories/Conditionals del
cuadrode diilogo Project options del paquete. Esta directiva habilita una ca-
racteristica adicional que no se obtiene por defecto: en tiempo de diseiio, el com-
ponente servidor tendra una propiedad adicional que lista como subelementos
todas las propiedades del servidor de Automatizacion:
Los metodos Set y Get de las propiedades de tipos complejos copian infor-
macion de las interfaces COM a 10s datos locales y luego desde estos a1 formula-
rio y viceversa. Los dos metodos de las cadenas, por ejemplo, hacen esto llamando
a las funciones Getolestrings y Setolestrings de Delphi. La aplica-
cion cliente usada para demostrar esta caracteristica se llama ListCli. Los dos
programas son complejos; per0 en lugar de mostrar aqui todos sus detalles, es
mejor que se estudie el codigo por si mismo, ya que 10s programadores de Delphi
no suelen utilizar esta tecnica.
El componente Container
Para crear una aplicacion contenedor COM en Delphi, hay que colocar un
componente Olecontainer en un formulario. A continuacion, hay que seleccio-
narlo y hacer clic con el boton derecho para activar su menu contextual, que
incluira una orden Insert Object. Al seleccionar esa orden, Delphi presenta el
cuadro de dialogo estandar Insert Object, que permite elegir entre una de las
aplicaciones servidor registradas en el ordenador.
Una vez insertado el objeto COM en el contenedor, el menu local del compo-
nente del contenedor mostrara varios elementos de menu personalizados que in-
cluyen ordenes para cambiar las propiedades del objeto COM, insertar otro,
copiarlo o eliminarlo. La lista contiene tambien 10s verbos o acciones del objeto
(como Edit, Open o Play). Despues de insertar un objeto COM en el contenedor,
el servidor correspondiente se pondra en marcha para permitir la edicion del
nuevo objeto. En cuanto se cierre la aplicacion servidor, Delphi actualizara el
objeto en el contenedor y lo mostrara en tiempo de diseiio en el formulario de la
aplicacion Delphi en desarrollo.
Si observamos la descripcion textual de un formulario que contenga un com-
ponente con un objeto dentro, se puede ver una propiedad Data,que contiene 10s
datos reales del objeto COM. Aunque el programa cliente almacene 10s datos del
objeto, no sabe como controlarlo y mostrarlo sin la ayuda del servidor apropiado
(que debe estar disponible en el ordenador en que se ejecute el programa). Esto
indica que el objeto esta incrustado.
Para soportar totalmente documentos compuestos, el programa deberia pro-
porcionar un menu y una barra de herramientas o un panel. Estos componentes
adicionales son importantes porque la edicion en el sitio supone una combinacion
de las interfaces del usuario del programa cliente y el programa servidor. Cuando
se active el objeto COM colocado, algunos de 10s menus desplegables pertene-
cientes a la barra de menu de la aplicacion servidor se incorporaran a la barra de
menu de la aplicacion contenedor.
La combinacion de menus es casi automatica en Delphi. Solamente hay que
definir 10s indices adecuados para 10s elementos de menu del contenedor, utilizan-
do la propiedad GroupIndex.Cualquier elemento de menu con un numero de
indice impar se sustituira por el elemento correspondiente del objeto OLE activo.
Mas especificamente, 10s menus desplegables File (0)y Window (4) pertenecen
a la aplicacion contenedor. Los menus desplegables Edit (A), View (3) y Help
(5) (0 10s grupos de menus desplegables con esos indices) son capturados por el
servidor COM. Se puede utilizar un sexto grupo llamado Object (2) para mostrar
otro menu desplegable mas entre 10s grupos Edit y View, cuando el objeto COM
este activo. El programa de demostracion OleCont que hemos escrito para demos-
trar estas caracteristicas permite a1 usuario crear un nuevo objeto llamando a1
metodo InsertObjectDialog de la clase Tolecontainer.
Despues de haber creado el objeto, podemos ejecutar su verbo principal utili-
zando el metodo DoVerb.El programa muestra tambien una pequeiia barra de
herramientas con algunos botones de mapas de bits. Hemos colocado algunos
componentes Tw inContro 1 en el formulario para asi permitir a1 usuario selec-
cionarlos y desactivar el Olecontainer.
Para mantener esta barra de herramientas o panel visible durante la edicion in
situ, hay que definir su propiedad Locked como True,lo que obliga a1 panel a
estar presente en la aplicacion y a que no lo sustituya una barra de herramientas
del servidor.
Para mostrar lo quc sucede a1 utilizar este metodo, hemos aiiadido a1 programa
un segundo panel con algunos botones mas. Dado que no hemos definido su pro-
picdad Locked,esta nueva barra de herramientas sera reemplazada por la del
servidor activo. Cuando la edicion in situ ponga en marcha una aplicacion servi-
dor que muestre una barra de herramientas, la del servidor reemplazara a la del
contenedor, como mucstra la parte inferior de la figura 12.6.
I
Figura 12.6. La segunda barra de herramientas del ejemplo OleCont (arriba) es
sustituida por la barra de herramientas del servidor (debajo).
Controles ActiveX
Visual Basic de Microsoft fue el primer entorno de desarrollo de programas en
presentar la idea de ofrecer componentes software a1 gran mercado, incluso aun-
que el concept0 de componentes software reciclables sea anterior a Visual Basic
(proccde de las teorias de la programacion orientada a objetos). El primer estandar
tecnico promovido por Visual Basic fue VBX, una especificacion de 16 bits que
estaba completamente disponible en Delphi 1. A1 pasar a las plataformas de 32
bits, Microsoft sustituyo el estandar VBX con 10s mas potentes y mas abiertos
controles ActiveX.
Figura 12.7. El ejernplo WordCont muestra como usar Autornatizacion con un objeto
incrustado.
__P
Login
1
Code Central
Qual~tyCentral
The Coad Letter
-
Two new communities StarTeam and
CaliberRM
Get Published Two new communltles have been added to
BOOKS BDN. Starlearn and CallberRM These two
communltles wrll show you how to utlllze
Developer Support StarTeam's automated change and
Shop conflguratlon management capabllltles and
Chat CallberRM's requirements deflntlon and
management features to galn control of your
Downloads development process and Increase your return
on Investment In software development
Search
Logm "Dawd I" (Dawd Interslmone!, Borland's vlce
President of Developer Relat~onsand Chef
Evangel~st
Soapbox
SIPfrom the Ftrehose 1 5may Interview with Steve Teixeira by Clay Shannon
Dawd lnterslmone Interview vwth Steve Teixe~ra,co-author of the classic "Delphi X
Behtnd the Screen Develooer's Gu~de'Lalona with Xavier Pachecol Amona other -I
Figura 12.8. El programa WebDemo despues de escoger una pagina muy conocida
para 10s desarrolladores de Delphi.
Empty Param es una OleVar iant predefinida que se puede utilizar siem-
pre que haya que pasar un valor predefinido como parametro de referencia. Es un
metodo abreviado muy util que se puede emplear para evitar crear una variable
o l e v a r i a n t vacia cada vez que haga falta un p a r h e t r o similar. El programa
llama a1 metodo Goto Page cuando el usuario hace clic sobre el boton Open File,
o cuando pulsa la tecla Intro mientras que se encuentra en el cuadro combinado o
cuando hace clic sobre el boton Go, como puede verse en el codigo fuente del
ejemplo. El programa controla tambien cuatro eventos pertenecientes al control
WebBrowser. Cuando termina la operacion de descarga, el programa actualiza el
texto de la barra de estado y tambien la lista desplegable del cuadro combinado:
procedure TForml.WebBrowserlDown10adComp1ete(Sender: TObject);
var
NewUrl : string;
begin
StatusBarl. Panels [0] .Text : = ' D o n e ' ;
// a d a d e URL a 1 c u a d r o c o m b i n a d o
NewUrl : = WebBrowserl.LocationURL;
if (NewUrl <> ' ' ) and (ComboURL.Items. IndexOf (NewUrl) < 0 )
then
ComboURL.Items.Add (NewUrl);
end;
2wlmpl2 pas
En este asistente simplemente hay que seleccionar la clase VCL que nos intere-
sa, personalizar 10s nombres que aparecen en 10s cuadros de texto y hacer clic
sobre el boton OK: Delphi construira el codigo fuente completo de un control
ActiveX.
El uso de las tres casillas de verificacion de la parte inferior de la ventana del
asistente puede no resultar obvio. Si hacemos que el control sea licenciado, Delphi
incluira una clave de licencia en el codigo y proporcionara este mismo GUID en
un archivo .LIC independiente. Este archivo de licencia es necesario para usar el
control en un entorno de diseiio sin la clave de licencia apropiada para el control
o para usarlo dentro de una pagina Web. La segunda casilla de verificacion per-
mite incluir informacion sobre la version para el ActiveX en el archivo OCX. Si
esta activada la tercera casilla de verificacion, el asistente aiiadira automaticamente
a1 control un cuadro Acerca de.
Si echamos un vistazo a1 codigo que genera el asistente, veremos que el ele-
mento clave es la creacion de una biblioteca de tipos y, por supuesto, una unidad
de importacion de la biblioteca de tipos correspondiente con la definicion de una
interfaz (dispinte r face) y otros tipos y constantes. En este ejemplo, el ar-
chive de importacion se llamax~rrowT L B . PAS.Lo mas aconsejable es estu-
diarlo para comprender corn0 define elp phi un control ActiveX. La unidad incluye
un GUID para el control, constantes para la definicion de 10s valores correspon-
dientes a 10s tipos COM enumerados utilizados por las propiedades del control
Delphi (como TxMdAr rowDir) y la declaracion de la interfaz IMdArrowX.
La parte final de la unidad de importacion incluye la declaracion de la clase
TMdArrowX. Se trata de una clase derivada de Tolecontrol que se puede
utilizar para instalar el control en Delphi, como se vio a1 principio de este capitu-
lo. No es necesaria para construir el control ActiveX, solo para instalarlo en
Delphi.
El resto del codigo y el que personalizaremos esta en la unidad principal, que
en el ejemplo se llama MdWArrowImpll. Esta unidad tiene la declaracion del
objeto servidor ActiveX, TMdWArrowX,que hereda de TActiveXControl e
implementa la interfaz especifica IMdWArrowX.Antes de personalizar este con-
trol, conviene ver su funcionamiento.
Primero hay que compilar la biblioteca ActiveX y luego registrarla con la
opcion de menu RuwRegister ActiveX Server de Delphi. Despues se puede
instalar el control como hemos hecho anteriormente, a excepcion de que hay que
especificar un nombre diferente para la nueva clase, para evitar conflictos de
nombres. Si se usa este control, no parecera muy diferente del control VCL origi-
nal, per0 la ventaja es que el mismo componente ahora puede instalarse en otros
entornos de desarrollo.
R &lax Helper OK
En el cuadro combinado hay que elegir entre una nueva propiedad, metodo o
evento. En el cuadro de edicion podremos escribir entonces la declaracion del
nuevo elemento de la interfaz. Si esta activada la casilla de verificacion Syntax
Helper, aparecera una sugerencia que describe lo que hay que escribir y resalta
10s errores. A1 definir un nuevo elemento de interfaz ActiveX, hay que tener en
cuenta que estamos limitados a 10s tipos de datos COM.
En el ejemplo XArrow hemos aiiadido dos propiedades a1 control ActiveX.
Dado que las propiedades P e n y B r u s h del componente original no son accesi-
bles, hemos hecho accesible su color. Veamos 10s ejemplos que se pueden escribir
en el cuadro de edicion del cuadro de dialogo (ejecutandolo dos veces):
property F i l l c o l o r : I n t e g e r ;
property P e n c o l o r : I n t e g e r ;
. . a . .
TRUCO:Del~hioro~orcionacuatro fichas de DroDkdades inteeradas Dara w r
en la unidad A c C t r l s .
. . . . . . y
. . . . #. . . .
. . . . . . . . .'. . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
Direction. adR~ghf(3)
11 II
.....................................
......................................
.....................................
......................................
.......................................
......................................
.......................................
Pencolor: New... I
.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
h o w point color: 0 NW. I
TRUCO: Otra sugerencia tiene que ver con el Capt ion del formulario de
la ficha de propiedades. Este se utiIizara en el cuadro de diiilogo de propie-
dades del entorno anfitrion como titulo de la solapa correspondiente a la
ficha de propiedades.
ActiveForms
Delphi proporciona una alternativa a1 uso del asistente ActiveX Control Wizard
para generar un control ActiveX. Podemos utilizar un ActiveForm que es un
control ActiveX basado en un formulario y que puede albergar uno o mas compo-
nentes de Delphi. Esta tecnica se usa en Visual Basic para crear nuevos controles
y tiene sentido si se quiere crear un componente compuesto.
En el ejemplo XClock, hemos colocado en un ActiveForm una etiqueta (un
control grafico que no puede usarse como punto de partida para un control ActiveX)
y un temporizador, y hemos conectado 10s dos con algo de codigo. El formulariol
control se convierte en un contenedor de otros controles, lo que facilita crear
componentes compuestos (es mas facil que para un componente compuesto VCL).
Para crear un control de este tipo, hay que seleccionar el icono ActiveForm de
la ficha ActiveX del cuadro de dialogo File>New. Delphi pedira informacion en
el cuadro de dialogo ActiveForm Wizard, que es similar a la asistente visto
anteriormente.
lnterioridades de ActiveForm
Antes de continuar con el ejemplo, examinaremos el codigo que genera el
ActiveForm Wizard. La principal diferencia con respecto a un simple formulario
de Delphi esta en la declaracion de la nueva clase formulario, que hereda de la
clase denominada TAct i v e Form e instala una interfaz ActiveForm especifica.
El codigo generado para la clase del formulario activo implementa unos cuantos
metodos G e t y S e t , que cambian o devuelven las propiedades correspondientes
a1 formulario Delphi; este codigo tambien implementa 10s eventos, que vuelven a
ser eventos del formulario.
Los eventos de T Form estan conectados con 10s metodos internos cuando se
crea el formulario. Por ejemplo:
procedure TAXForml.Initialize;
begin
OnActivate : = ActivateEvent;
end ;
Cada evento se proyecta a si mismo sobre el evento del ActiveX externo, como
en el siguiente metodo:
procedure TAXForml.ActivateEvent(Sender: TObject);
begin
if FEvents <> nil then FEvents .OnActivate;
end;
3 LKto rrqJ M K
Figura 12.10. El control XClock en la pagina HTML de muestra.
Hay que fijarse en que en la parte del archivo HTML que se refiere a1 control
se puede usar la etiqueta especial param para personalizar las propiedades del
control. Por ejemplo, en el archivo HTML del control XArrow, hemos modifica-
d o el a r c h i v o H T M L generado automaticamente (en el archivo
XArrowCus t .htm) con estas tres etiquetas param:
Ademas de crear servidores basicos COM, Delphi tambien permite crear obje-
tos COM mejorados, incluyendo objetos sin estado y soporte de transacciones.
Este tipo de objeto COM fue presentado por Microsoft con las siglas MTS
(Microsoft Transaction Sewer) en Windows NT y 98 y renombrado posterior-
mente como COM+ en Windows 2000lXP. (Aqui hablaremos de COM+, per0 es
lo mismo.) Delphi soporta la creacion de objetos estandar sin estado y modulos de
datos remotos DataSnap basados en objetos sin estado. En ambos casos, empeza-
remos el desarrollo utilizando uno de 10s asistentes disponibles de Delphi, usando
para ello el cuadro de dialog0 New Items y seleccionando el icono Transactional
Object de la ficha ActiveX o el icono Transactional Data Module de la ficha
Multitier. Estos objetos se deben afiadir a un proyecto de biblioteca ActiveX, no a
una aplicacion base. El icono COM+ Event Object se utiliza para soportar 10s
eventos COM+. COM+ ofrece un entorno en tiempo de ejecucion que soporta
servicios de transaccion de bases de datos, seguridad, reserva de recursos y una
mejora global en la robustez de las aplicaciones DCOM. El entorno en tiempo de
ejecucion se encarga de manejar objetos llamados componentes COM+. Se trata
de objetos COM guardados en un servidor en proceso (es decir, una DLL). Mien-
tras que otros objetos COM se ejecutan directamente en la aplicacion cliente, 10s
objetos COM+ se manejan en este entorno en tiempo de ejecucion, en el que se
instalan las bibliotecas COM+. Los objetos COM+ deben soportar interfaces
COM especificas, comenzando por IObjectControl, que es la interfaz base (como
IUnknown para un objeto COM).
Antes de entrar en detalles demasiado tecnicos y de bajo nivel, consideremos
COM+ desde una perspectiva distinta: las ventajas de este enfoque. COM+ pro-
porciona unas cuantas caracteristicas interesantes:
Seguridad basada en funciones: La funcion asignada a un cliente deter-
mina si este tiene el derecho de acceso a una interfaz o a un modulo de
datos.
Recursos de bases de datos reducidos: Se puede reducir el numero de
conexiones a una base de datos, ya que 10s registros de la capa intermedia
se conectan a1 servidor y utilizan las mismas conexiones para varios clien-
tes (aunque no es posible tener mas clientes conectados que licencias para
el servidor).
Transacciones de bases de datos: El soporte COM+ de transacciones
incluye operaciones en bases de datos multiples, aunque pocos servidores
SQL, ademas de 10s de Microsoft, soportan transacciones COM+.
Creacion de un componente COM+
Para crear un componente COM+ debemos comenzar creando el proyecto de
biblioteca de un control ActiveX. DespuCs seguimos estos pasos:
1. Seleccionamos un nuevo Transactional Object en la ficha ActiveX del cua-
dro de dialogo New Items.
2. En el cuadro de dialogo resultante (ver figura 12.1 I), escribimos el nombre
del nuevo componente (ComPlusl Object, en el ejemplo ComPlus 1).
Figura 12.11. El cuadro de dialogo New Transactional Object, utilizado para crear un
objeto COM+.
Una aplicacion COM+ no es nada mas que una manera de agrupar componen-
tes COM+; no se trata de un programa ni nada parecido (lo que hace que no quede
claro por que se le llama aplicacion). Por eso, en el cuadro de dialogo Install
COM+ Object, se puede escoger una aplicacion/grupo existente, seleccionar la
pagina Install Into New Application, y escribir un nombre y una descripcion.
Hemos llamado a la aplicacion COM+ La biblia de Delphi 7 Przrebn, tal y
como muestra la figura 12.12 en la consola de administracion de Servicios de
componentes de Microsoft. Este es el terminal que se puede usar para ajustar el
comportamiento de sus componentes COM+, estableciendo su modelo de activa-
cion (activacion en el instante, reserva de objetos, y otros), su soporte de transac-
cion y 10s modelos de seguridad y concurrencia que se desea usar. Tambien se
puede usar esta consola para vigilar 10s objetos y las llamadas de metodos (en
caso de que tarden mucho tiempo en ejecutarse). En la figura 12.12 se puede ver
que hay dos objetos activos.
-*
-
l
i
Apkadonc. COM+
&, .wrLnmie5
l . ... (3E93EF3-CE24-4
a ~ n r n ~ l u sCmR ... Nose ad ... L Necesar~o Subpro. ..
[i COM+ Expbrcr
i
3 & <OM+ QC Dead Ldrn C
-
E
- COM+ LnY~es
.?$ ~a btbla de D&
+AcCorrponcr*r
:-.
7 (ck~
a CmFlurl .ComFi
*. 'J Interkc5
:+< 1susmpoone
i
, cTe5 n ,
I
. 4-.. -- -- . ---
7-
Figura 12.12. El componente COM+ recien instalado en una aplicacion COM+ (tal y
como lo muestra la herramienta Servicios de componente de Microsoft).
-. - -= ---- - -
1 ADVERTENCIA: Ya que se ha; ireado uno o mits objetw, la bibli&ea I
I COM sigue wgada en el entomo COM+ y alpnos de 10s objctos putden I
P pen0 haya clhtsp conscta-
dos a euos. ror esre mouvo, generameme no se pude rc~ompilmb biblio-
teca COM despds de usarla, a no ser que se use M M C para cmafla o se
I establezca un Transaction Timeout de 0 segundosa MMC. ' I
Hemos creado un programa cliente para el objeto COM+, per0 es igual que
cualquier otro cliente COM de Delphi. Despues de importar la biblioteca de tipos,
que se registra automaticamente mientras se instala el componente, hemos creado
una variable de tip0 de interfaz que hace referencia a ella y llamada por sus
metodos como es habitual.
Para crear un evento COM+, deberiamos crear una biblioteca COM (o biblio-
teca ActiveX) y utilizar el asistente COM+ Event Object. El proyecto resultante
contendra una biblioteca de tipos con la definicion de la interfaz utilizada para
activar 10s eventos ademas de algo de codigo de implementacion falso. El servidor
que reciba la notificacion de 10s eventos proporcionara la implementacion de la
interfaz. El codigo falso se encuentra ahi solo para soportar el sistema de registro
COM de Delphi.
Mientras construiamos la biblioteca MdComEvents, hemos aiiadido a la bi-
blioteca de tipos un metodo sencillo con dos parametros, que han dado lugar a1
siguiente codigo (en el archivo de definicion de la interfaz):
type
IMdInf orm = interface (IDispatch)
[ ' (202D2CC8-8E6C-4E96-9C14-1FAAE392OECC}' ]
p r o c e d u r e Informs(Code: Integer; const Message:
Widestring); safecall;
end;
La unidad principal incluye el objeto COM falso (el metodo es abstracto, de
manera que no tiene implementacion) y su factoria de clase, para permitir que el
servidor se registre a si mismo. Se puede compilar la biblioteca e instalarla en el
entorno COM+, siguiendo estos pasos:
1. En la consola de Servicios de componentes de Microsoft, se escoge una
aplicacion COM+, vamos a la carpeta Componentes y usamos el menu
contextual para aiiadir un nuevo componente.
2. En el asistente para instalacion de componentes COM+, hacemos clic so-
bre el boton Instalar nuevas clases de eventos y seleccionamos la biblio-
teca que acabamos de compilar. La definicion del evento COM+ se instalara
automaticamente.
Para comprobar si funciona, tendremos que crear una implementacion de esta
interfaz de evento y un cliente que la invoque. La implementacion puede aiiadirse
a otra biblioteca ActiveX, albergando un objeto COM basico. Dentro del COM
Object Wizard de Delphi, podemos seleccionar la interfaz a implementar, esco-
giendola de la lista que aparece a1 hacer clic sobre el boton List.
La biblioteca resultante, que en el ejemplo se llama EvtSubscriber, expone
un objeto de Autornatizacion a un objeto COM que implemente la interfaz
IDispatch (que es obligatorio para 10s eventos COM+). El objeto tiene la siguien-
te definicion y codigo:
type
TInformSubscriber = class (TAutoObject, IMdInf orm)
protected
procedure Informs (Code: Integer; const Message:
WideString) ; safecall;
end;
+* Bl5 5 -@-I
J Ralr dr r ~ n s d a
- @j Scrvms de componentes w e Id dcntdaz
- _. I Scmdu- -
- JEQUIWS
@$
- gMPc
.+:
_1 A p k a c m z COM+
El UYes
- &ma
-
I
4& r1ptk.a;
+ 6 COM+ Explaer
d @ COM+ QC Dead Lettn C
$ cm+ Mlke5
La M a & Debh 7 (der
-
. O COmpanenteS
8- a cmPCI51c a u
+ '
JLnterfaer
*I - -- - - - - -
17
1
Figura 12.13. Un evento COM+ con dos suscripciones en la consola Servicios de
componentes.
uses
NetNurnberClass in 'NetNumberClass.pasl;
begin
/ / c r e a u n o b j e t o p a r a e n l a z a r t o d o e l codigo
TNumber.Create;
end.
D:\md7code\l2\NetImport>tlibimp NetLibrary.dl1
Borland TLIBIMP Version 7.0
Copyright (c) 1997, 2002 Borland Software Corporation
Type library loaded . . . .
Created D:\rnd7code\l2\NetImport\mscorlib~TLBBdcr
Created D:\md7code\12\NetImport\mscorlib~TLBBpas
Created D:\md7code\12\NetImport\NetLibrary~TLBBdcr
Created D:\rnd7code\l2\NetImport\NetLibrary~TLBBpas
El efecto es crear una unidad para la biblioteca de tipos del proyecto y una
unidad para la Microsoft .NET Core Library importada ( m s c o r l i b .d l 1).Ahora
podemos crear una nueva aplicacion de Delphi 7 (un programa Win32 estandar) y
usar 10s objetos .NET como si fueran objetos COM. Este es el codigo del ejemplo
NetImport, que se muestra en la figura 12.14:
uses
NetLibrary-TLB;
Figura 12.14. El programa Netlmport usa un objeto .NET para sumar nlimeros
Parte Ill
Arquitecturas
orientadas
a bases de
datos en Delphi
Arquitectura
de bases de
datos Delphi
Como una solucion mas, para las aplicaciones simples puede usarse el compo-
nente ClientDataSet de Delphi, que permite guardar tablas en archivos locales
1
(algo que Borland llama MyBase). Fijese en que una tipica aplicacion de Delphi
basada en tablas Paradox no puede adaptarse a Kylix, debido a su carencia de
BDE.
La biblioteca dbExpress
Una de las caracteristicas mas importantes nuevas en Delphi en 10s aiios mas
recientes es la introduccion de la biblioteca de la base de datos dbExpress (DBX),
disponible tanto para Linux como para Windows. Se trata de una biblioteca y no
de un motor de bases de datos porque, a1 contrario que otras soluciones, dbExpress
utiliza un enfoque ligero y basicamente no necesita ninguna configuration en las
maquinas de 10s usuarios finales.
Las caracteristicas claves de dbExpress y las razones por las que Borland la
ha introducido, junto con el desarrollo del proyecto Kylix son su ligereza y
portabilidad. En comparacion con otras bases de datos muy potentes, dbExpress
resulta algo limitada en cuanto a sus capacidades.
dbEspress solo puede acceder a servidores SQL (no a archivos locales); no
tiene capacidad para guardar copias temporales para acelerar 10s procesos y solo
proporciona un acceso a datos unidireccional. Solo puede trabajar originariamen-
te con consultas SQL y no puede crear las sentencias de actualizacion SQL co-
rrespondientes.
La primera impresion que producen estas limitaciones es que la biblioteca
podria resultar inutil. Nada mas lejos de la realidad: se trata de caracteristicas
que la hacen interesante. Los con.juntos de datos unidireccionales sin actualiza-
cion directa son lo normal si se necesita generar informes, incluyendo la genera-
cion de paginas HTML que muestren el contenido de una base de datos. Sin
embargo, si se desea construir una interfaz de usuario para editar 10s datos, Delphi
incluye componentes especificos (ClientDataSet y Provider, para ser precisos)
que permiten la edicion en cache y la resolucion de consultas SQL.
Estos componentes permiten que la aplicacion basada en dbExpress tenga un
mayor control del que podemos tener con un motor de base de datos independiente
(monolitico), que realiza automaticamente acciones adicionales de un mod0 no
personalizable.
dbEspress permite escribir una aplicacion que, escepto 10s problemas deriva-
dos de 10s diversos dialectos SQL, puede acceder a muchos motores de bases de
datos distintos sin realizar mucha modification del codigo. Entre 10s servidores
SQL soportados en Delphi 7 se incluye la propia base de datos InterBase de
Borland, el servidor de bases de datos de Oracle, la base de datos MySQL (que es
muy popular en Linux), Informix, DB2 de IBM, y SQL Server de Microsoft. En
este capitulo vamos a centrarnos en las bases de la arquitectura de las bases de
datos.
o b j e c t Forml: TForml
Activecontrol = DBGridl
Caption = ' M y B a s e l '
OnCreate = Formcreate
o b j e c t DBGrid: TDBGrid
DataSource = DataSourcel
end
o b j e c t DataSourcel: TDataSource
DataSet cds
end
o b j e c t cds: TClientDataSet
FileName = ' C: \Archives d e p r o g r a m \Archives
Comunes \ B o r l a n d Shared\Da t a \ C u s t o m e r . c d s '
end
end
1510 Ocean Pi
1551 M m l D
1624 MakaiSC
1645 Action Ck
1651 Jamaica L ,,- ,.,
1680 l s l d Finders 6133 1/3 Stone Avenue
1984 Adventure Undersea W Box 744
2118 Blue Spalo Ckrb 63365 Nez Perce S l m t
2135 Frank's Divers Supply 1455 North 44th St.
I d
l
Figura 13.1. Una tabla local de rnuestra activa en tiempo de disefio dentro del IDE de
Delphi.
I tarse correctamente.
I
La bibliotcca Midas es una biblioteca en lenguaje C, per0 desde Delphi 6
puede enlazarse directamente con un ejecutable a1 incluir la unidad M i d a s L i b
(una DCU especial producida por un compilador de C). En este caso, no sera
necesario distribuir la biblioteca en formato DLL.
cie de formateo XML para pobres: el c6digo aiiade una nueva linea a1 final.de
cada etiqueta XML afiadiendo una nueva liiea tras la marca de cierre. La figura
13.2 muestra la representacion XML con unos cuantos registros.
<PARAMS/)
</METADATA>
<ROWDATA>
<ROW om-"one" Iwo="lU/>
<ROW one.'koMIwo="2"/>
<ROWm="ne"lwa-"lV'l,
</ROWDATA)
t/DATAPACMT>
lndexado
Una vez que se ticne un C l i e n t D a t a S e t en mcmoria, se pueden realizar
muchas operaciones sobre el. Las mas simples son el indexado, filtrado y busque-
da de registros; entre las operaciones mas complejas se incluyen la agrupacion, la
definicion de valores agregados y la gestion del registro de cambios. Dejaremos
10s temas mas compkjos para el final de capitulo.
Indexar un C l i e n t D a t a S e t es una cuestion de establecer la propiedad
I n d e x F i e l d N a m e s . Suele hacerse cuando el usuario hace clic sobre el campo
de titulo en un componente DBGrid (con lo que se lanza el evento O n T i t l e -
c l i c k ) , como en el ejemplo MyBase2:
tienen en memoria. Por este motivo, 10s indices 10s consideran como sim-
ples campos.
Filtrado
Al igual que con cualquier otro conjunto de datos, podemos usar la propiedad
Filter para especificar la inclusion en el conjunto de datos de partes de 10s
datos a las que esta ligado el componente. La operation de filtrado ocupa memo-
ria despues de cargar todos 10s registros, asi que se trata de una manera de mos-
trar menos datos al usuario, no de limitar la ocupacion de memoria de un conjunto
de datos local grande.
Cuando queremos conseguir una gran cantidad de datos desde un servidor (en
una arquitectura clientelservidor) deberiamos usar una consulta adecuada de mod0
que no recuperemos un gran conjunto de datos de un servidor SQL. La mejor
opcion deberia ser normalmente el filtrado desde la salida del servidor. Con 10s
datos locales, se puede tener en cuenta el partir un gran numero de registros en un
conjunto de distintos archivos, para que se puedan cargar solo 10s necesarios y no
todos.
Sin embargo, el filtrado local en el ClientDataSet puede resultar muy
util, sobre todo porque las espresiones de filtro que podemos usar con este com-
ponente son mucho mas amplias que aquellas que podemos usar con otros conjun-
tos dc datos. En concreto, podemos usar lo siguiente:
La comparacion estandar y 10s operadores Iogicos ( ~ o p 1uation > 1000
and Area < 1000).
Operadores aritmeticos ( Population / Area < 10 ) .
Funciones de cadena (Substring(Last-Name, 1, 2 ) = Ca ' )
Funcionesdefechayhora (Year (Invoice-Date) = 2002)
Otras, como la funcion Like, comodines y un operador In.
Estas prestaciones de filtrado se encuentran perfectamente documentadas en el
archivo de ayuda de la VCL: Deberia buscarse la pagina "Limiting what records
appear" vinculada a la descripcion de la propiedad F i l t e r de la clase
T C l i e n t DataSet, o llegar a ella desde la pagina Help Contents, siguiendo
esta cadena: Developing Database ApplicationsAJsing client datasets>
Limiting what records appear.
Busqueda de registros
El filtrado permite limitar 10s registros que se muestran a1 usuario del progra-
ma, per0 muchas veces se querran mostrar todos 10s registros y acceder unica-
mente a uno especifico. El metodo Locate se encarga de esto. Si jamas se ha
usado Locate, un primer vistazo a1 archivo de ayuda no dejara las cosas muy
claras. La idea es que hay que proporcionar una lista de 10s campos que se quieren
buscar y una lista de valores, uno para cada campo. Si se desea buscar una
correspondencia con un unico campo, el valor se pasa directamente. como en este
caso en que la cadena de busqueda se encuentra en el componente EditName):
procedure TForml.btnLocateClick(Sender: TObject);
begin
i f not cds .Locate ( ' L a s t N a m e l, EditName. Text, [I ) then
MessageDlg ( ' " ' + EditName.Text + ' " not f o u n d ' , mtError,
[ r n b o k l , 0);
end;
Si se busca mediante varios campos, hay que pasar una matriz variante con la
lista de valores para 10s que se desea correspondencia. La matriz variante puede
crearse desde una matriz constante con la funcion VarArrayOf o a partir de la
nada mediante la llamada VarArra yCrea t e . Este es un fragment0 del codigo:
cds. Locate ( ' LastName;FlrstName' , VarArrayOf ( ['Cook',
'Kevin' ] ) , [I )
Por ultimo, se puede usar el mismo metodo para buscar un registro incluso
aunque solo se conozca el principio del campo que se esta buscando. Todo lo que
hay que hacer es aiiadir el indicador l o p a r t ialKe y a1 parametro o p t i o n s
(el tercero) de la llamada a Locate.
NOTA: Usar Locate tiene sentido cuando se trabaja con una tabla local,
pero no se adapta bien a las aplicaciones cliente/servidor. En un servidor
SQL, tknicas sirnilares por parte del cliente implican llevar en primer lu-
gar todos 10s datos a la aplicaci6n cliente (lo que es generalmente una rnala
idea) y buscar despues un registro especifico. Deberian localizarse 10s da-
tos mediante sentencias SQL restringidas. A h se puede usar Locate des-
puis de obtener un conjunto de datos limitado. Por ejemplo, se puede buscar
ciudad o zona dadas, con lo que se conseguira un conjunto de resultados de
tamailo reducido.
Deshacer y Savepoint
Cuando un usuario modifica 10s datos de un componente C l i e n t Da t a Se t ,
las actualizaciones se almacenan en una zona de memoria llamada D e l t a . El
motivo de hacer un seguimiento de 10s cambios del usuario en lugar de conservar
la tabla resultante se debe a1 mod0 en que se manejan las actualizaciones en una
arquitectura clientelservidor.
En este caso, el programa no tiene que enviar toda la tabla de vuelta al servi-
dor, sino solo una lista con 10s cambios del usuario (mediante sentencias SQL
especificas).
Ya que el componente C l i e n t D a t a S e t sigue la pista de 10s cambios, se
pueden rechazar esos cambios, eliminando entradas del delta. El componente po-
see un metodo U n d o L a s t C h a n g e especifico para ello. El parametro
F o l l o w c h a n g e de este metodo permite seguir la operacion deshacer (el con-
junto de datos del cliente se movera a1 registro que se ha recuperado mediante la
operacion deshacer). Veamos el codigo usado para conectar un boton Undo:
procedure TForml.ButtonUndoClick(Sender: TObject);
begin
.
cds UndoLastChange (True);
end :
El ejemplo DbAware
El ejemplo DbAware resalta el uso de un control D B R a d i o G r o u p con 10s
parametros comentados en la seccion anterior y un control D B C h e c k B o x . Este
ejemplo no es mucho mas complejo que 10s anteriores, pero tiene un formulario
con controles data-aware orientados a campo, en lugar de una cuadricula que 10s
englobe todos. La figura 13.3 muestra el formulario del ejemplo en tiempo de
diseiio.
A1 igual que en el programa MyBase2, la aplicacion define su propia estructu-
ra de tabla, mediante la propiedad de conjunto F i e l d D e f s del ClientDataSet.
La tabla 13.1 proporciona un breve resumen de 10s campos definidos.
AddRandwnOaa
- --
I - - . .
* 41
\,
Q'
D d a S o w l -cdt -- -
I r Management
. - -.
Figura 13.3. Los controles data-aware del ejemplo DbAware en tlempo de diseiio.
LastName ft String
FirstName ftString
Department ftsmallint
Branch ftString
Senior ftBoolean
HireDate ftDate
El programa tiene algo de codigo para rellenar la tabla con valores aleatorios.
Este codigo es aburrido y no demasiado complejo, por lo que no vamos a comen-
tar 10s detalles, pero se puede analizar el codigo fuente de DbAware si se tiene
interes.
El componente DataSet
En lugar de pasar a analizar las prestaciones de un conjunto de datos especifi-
cos, hemos preferido dedicar algo de espacio a una introduccion general a las
caracteristicas de la clase T D a t a S e t , que comparten todas las clases heredadas
de acceso a datos. El componente DataSet es bastante complejo, por lo tanto, no
enumeraremos todas sus capacidades sin0 solo sus elementos principales.
La idea tras este componente es la de proporcionar acceso a una serie de
registros que se leen desde alguna fuente de datos, se guardan en buffers internos
(por razones de rendimiento) y que el usuario podria modificar, con la posibilidad
de volver a escribir 10s cambios almacenandolos de forma permanente. Este enfo-
que es lo suficientemente genkrico como para que se pueda aplicar a tipos diferen-
tes de datos (incluso datos que no esten en bases de datos) per0 hay que seguir
unas ciertas normas:
En primer lugar, solo puede haber un registro activo en cada momento, por
lo que si tenemos que acceder a datos que estan en diversos registros,
debemos movernos a cada uno de ellos, leer 10s datos, desplazarnos de
nuevo y asi sucesivamente.
En segundo lugar, solo podemos editar el registro activo: no podemos mo-
dificar un conjunto de registros a1 mismo tiempo, como hacemos en las
bases de datos relacionales.
Podemos modificar 10s datos del buffer activo solo despues de haber decla-
rado explicitamente que deseamos hacerlo asi, dando la orden E d i t a1
conjunto de datos. Tambien podemos usar la orden I n s e r t para crear un
nuevo registro en blanco y cerrar ambas operaciones (de insercion o edi-
cion) con la orden PO s t .
Otros elementos interesantes de un conjunto de datos que analizaremos mas
adelante son su estado (y 10s eventos de cambio de estado), las posiciones de
navegacion y de registros, y el papel de 10s objetos de campo. Como resumen de
las prestaciones del componente Da t aSe t , en el listado 13.2 hemos incluido 10s
metodos publicos de la clase D a t a s e t (donde se ha editado y comentado el
codigo por claridad). No todos estos metodos se utilizan directamente de manera
habitual, per0 aun asi, hemos preferido mostrarlos todos.
public
/ / c r e a y d e s t r u y e , a b r e y cierra
c o n s t r u c t o r Create (AOwner: TComponent) ; override;
d e s t r u c t o r Destroy; override;
p r o c e d u r e Open;
p r o c e d u r e Close;
p r o p e r t y Beforeopen: TDataSetNotifyEvent r e a d FBeforeOpen
w r i t e FBeforeOpen;
p r o p e r t y Afteropen: TDataSetNotifyEvent r e a d FAfterOpen
w r i t e FAfterOpen;
p r o p e r t y Beforeclose: TDataSetNotifyEvent
r e a d FBeforeClose w r i t e FBeforeClose;
p r o p e r t y Afterclose: TDataSetNotifyEvent r e a d FAfterClose
w r i t e FAfterClose;
// i n f o r m a c i o n s o b r e el estado
f u n c t i o n IsEmpty: Boolean;
p r o p e r t y Active: Boolean r e a d GetActive w r i t e SetActive
d e f a u l t False;
p r o p e r t y State: TDataSetState r e a d FState;
function ActiveBuffer: PChar;
property IsUniDirectional: Boolean
read FIsUniDirectional write FIsUniDirectional default
False;
function Updatestatus: TUpdateStatus; virtual;
property Recordsize: Word read GetRecordSize;
property Objectview: Boolean read FObjectView write
SetOb jectView;
property Recordcount: Integer read GetRecordCount;
function IsSequenced: Boolean; virtual;
function IsLinkedTo(DataSource: TDataSource): Boolean;
// f u e n t e d e d a t o s
property Datasource: TDataSource read GetDataSource;
procedure DisableControls;
procedure EnableCont rols ;
function ControlsDisabled: Boolean;
/ / c a m p o s , corno b l o b s , d e t a l l e s , c a l c u l a d o s y o t r o s
function FieldByName(c0nst FieldName: string): TField;
function FindField (const FieldName: string) : TField;
procedure GetFieldList (List: TList; const FieldNames:
string) ;
procedure Get FieldNames (List: TStrings) ; virtual; // v i r t u a l
// d e s d e D e l p h i 7
property Fieldcount: Integer read GetFieldCount;
property FieldDefs: TFieldDefs read FFieldDefs write
SetFieldDef s;
property FieldDefList: TFieldDefList read FFieldDefList;
property Fields : TFields read FFields;
property FieldList: TFieldList read FFieldList;
property FieldValues[const FieldName: string]: Variant
read GetFieldValue write SetFieldValue; default;
property AggFields: TFields read FAggFields;
property DataSetField: TDataSetField
read FDataSetField write SetDataSetField;
property DefaultFields: Boolean read FDefaultFields;
procedure ClearFields;
function GetBlobFieldData(FieldN0: Integer;
var Buffer: TBlobByteData): Integer; virtual;
function CreateBlobStream(Fie1d: TField;
Mode: TBlobStreamMode) : TStream; virtual;
function GetFieldData(Fie1d: TField;
Buffer : Pointer) : Boolean; overload; virtual;
procedure GetDetailDataSets(List: TList); virtual;
procedure GetDetailLinkFields(MasterFields, DetailFields:
TList) ; virtual;
function GetFieldData(FieldN0: Integer;
Buffer: Pointer) : Boolean; overload; virtual;
function GetFieldData(Fie1d: TField; Buffer: Pointer;
NativeFormat: Boolean):
Boolean; overload; virtual;
property AutoCalcFields: Boolean
read FAutoCalcFields write FAutoCalcFields default True;
property OnCalcFields: TDataSetNotifyEvent
read FOnCalcFields write FOnCalcFields;
// p o s i c i d n , m o v i m i e n t o
procedure CheckBrowseMode;
procedure First;
procedure Last;
procedure Next;
procedure Prior;
function MoveBy(Distance: Integer): Integer;
property RecNo: Integer read GetRecNo write SetRecNo;
property Bof: Boolean read FBOF;
property Eof: Boolean read FEOF;
procedure CursorPosChanged;
property BeforeScroll: TDataSetNotifyEvent
read FBeforeScroll write FBeforeScroll;
property Afterscroll: TDataSetNotifyEvent
read FAfterScroll write FAfterScroll;
// marcadores
procedure FreeBookmark(Bookmark: TBookmark); virtual;
function GetBookmark: TBookmark; virtual;
function BookmarkValid(Bookmark: TBookmark): Boolean;
virtual ;
procedure GotoBookmark (Bookmark: TBookmark) ;
function CompareBookmarks(Bookmark1, Bookmark2: TBookmark):
Integer; virtual;
property Bookmark: TBookmarkStr read GetBookmarkStr write
SetBookmarkStr;
// b u s c a , l o c a l i z a
function FindFirst: Boolean;
function FindLast: Boolean;
function FindNext: Boolean;
function Findprior: Boolean;
property Found: Boolean read GetFound;
function Locate(const KeyFields: string; const KeyValues:
Variant;
Options: TLocateOptions): Boolean; virtual;
function Lookup(const KeyFields: string; const KeyValues:
Variant;
const ResultFields: string) : Variant; virtual;
// f i l t r a d o
property Filter: string read FFilterText write
SetFilterText;
property Filtered: Boolean read FFiltered write SetFiltered
default False;
property FilterOptions: TFilterOptions
read FFilterOptions write SetFilterOptions default [ I ;
property OnFilterRecord: TFilterRecordEvent
read FOnFilterRecord write SetOnFilterRecord:
// r e f r e s c a , a c t u a l i z a
procedure Refresh;
property BeforeRefresh: TDataSetNotifyEvent
read FBeforeRefresh write FBeforeRefresh;
property AfterRefresh: TDataSetNotifyEvent
read FAfterRefresh w r i t e FAfterRefresh;
procedure UpdateCursorPos;
procedure UpdateRecord;
function GetCurrentRecord(Buffer: PChar): Boolean; v i r t u a l ;
procedure Res ync (Mode: TResyncMode) ; v i r t u a l ;
// e d i t a , i n s e r t a , e n v i a y b o r r a
property CanModify: Boolean read GetCanModify;
property Modified: Boolean read modified;
procedure Append;
procedure Edit;
procedure Insert ;
procedure Cancel; v i r t u a l ;
procedure Delete;
procedure Post ; v i r t u a l ;
procedure AppendRecord (const Values : a r r a y of const) ;
procedure InsertRecord (const Values : a r r a y of const) ;
procedure SetFields(const Values: a r r a y of c o n s t ) ;
// e v e n t o s r e l a c i o n a d o s c o n e d i t a r , i n s e r t a r , e n v i a r y
// b o r r a r
property BeforeInsert: TDataSetNotifyEvent
read FBeforeInsert w r i t e FBeforeInsert;
property AfterInsert: TDataSetNotifyEvent
read FAfterInsert w r i t e FAfterInsert;
property BeforeEdit: TDataSetNotifyEvent read FBeforeEdit
w r i t e FBeforeEdit;
property AfterEdit: TDataSetNotifyEvent read FAfterEdit
w r i t e FAfterEdit;
property BeforePost: TDataSetNotifyEvent read FBeforePost
w r i t e FBeforePost;
property AfterPost: TDataSetNotifyEvent read FAfterPost
w r i t e FAfterPost;
property Beforecancel: TDataSetNotifyEvent
read FBeforeCancel w r i t e FBeforeCancel;
property Aftercancel: TDataSetNotifyEvent
read FAfterCancel w r i t e FAfterCancel;
property BeforeDelete: TDataSetNotifyEvent
read FBeforeDelete w r i t e FBeforeDelete;
property AfterDelete: TDataSetNotifyEvent
read FAfterDelete w r i t e FAfterDelete;
property OnDeleteError: TDataSetErrorEvent
read FOnDeleteError w r i t e FOnDeleteError;
property OnEditError: TDataSetErrorEvent
read FOnEditError w r i t e FOnEditError;
property OnNewRecord: TDataSetNotifyEvent
read FOnNewRecord w r i t e FOnNewRecord;
property OnPostError: TDataSetErrorEvent
r e a d FOnPostError w r i t e FOnPostError;
/ / soporte, utilidades
f u n c t i o n Translate (Src, Dest: PChar;
ToOem: Boolean): Integer; virtual;
property Designer: TDataSetDesigner r e a d FDesigner;
p r o p e r t y BlockReadSize: Integer r e a d FBlockReadSize write
SetBlockReadSize;
property SparseArrays: Boolean r e a d FSparseArrays write
SetSparseArrays;
end;
El estado de un Dataset
Cuando se trabaja sobre un conjunto de datos en Delphi, podemos trabajar en
distintos estados que nos indicara la propiedad especifica St ate, a la que pode-
mos dar diferentes valores:
dsBrowse: Indica que el conjunto de datos esta en un mod0 de navegacion
normal y se usa para ver 10s datos e inspeccionar 10s registros.
dsEdit: Indica que el conjunto de datos esta en mod0 de edicion. Un con-
junto de datos entra en este estado cuando el programa llama a1 metodo
Edit o cuando el Datasource tiene la propiedad A u t oEdi t configurada
como T r u e y el usuario comienza a editar un control data-aware, como
un DBGrid o DBEdit. Cuando se envia el registro que ha cambiado, el
conjunto de datos abandona el estado dsEdit.
dsInsert: Indica que se esta aiiadiendo un nuevo registro a1 conjunto de
datos. Esto podria ocurrir cuando se llama a 10s metodos Insert o
Append, moviendo la ultima linea de un componente DBGrid o usando la
orden correspondiente del componente DBNavigator.
dsInactive: Es el estado de un conjunto de datos cerrado.
dsCalcFields: Es el estado de un conjunto de datos mientras se esta reali-
zando el calculo de un campo, es decir, durante una llamada a un controla-
dor de eventos OnCalcFields.
dsNewValue, dsOldValue y dsCurValue: Son 10s estados de un conjunto
de datos cuando se esta actualizando la cache.
dsFilter: Es el estado de un conjunto de datos mientras se esta definiendo
un filtro, es decir durante una llamada a un controlador de eventos
OnFilterRecord.
Value es una propiedad de tipo variante, por lo que resulta en cierto mod0
mas eficiente usar propiedades de acceso especificas de tipo. El componente de
conjunto de datos tiene tambien una propiedad de metodo abreviado para acceder
a1 valor de tipo variante de un campo, la propiedad predefinida FieldValues.
A1 ser una propiedad predefinida significa que podemos omitirla en el codigo
aplicando directamente 10s corchetes a1 conjunto de datos:
strName : = c d s .Fieldvalues [ ' L a s t N a m e '] ;
strName : = cds [ ' L a s t N a m e l] ;
Crear 10s componentes de campo cada vez que se abre un conjunto de datos es
solo un comportamiento predefinido. Como alternativa, podemos crear 10s com-
ponentes de campo en tiempo de disefio, usando el editor Fields (para ver este
editor en funcionamiento, hay que hacer doble clic sobre un conjunto de datos, o
activar su menu local o el de la vista Object TreeView y escoger la opcion
Fields Editor). Despues de crear un campo para la columna LastName de una
tabla, por ejemplo, podemos referirnos a su valor aplicando uno de 10s metodos
AsXxx a1 objeto de campo adecuado:
Name
La orden Define del editor Fields permite definir un nuevo campo calcula-
do, un campo de busqueda o un campo con un tipo modificado. En este cuadro de
dialogo, se puede escribir un nombre de campo descriptivo, que podria incluir
espacios en blanco.
Delphi genera un nombre interno (el nombre del componente de campo) que
ademas se puede personalizar. A continuacion, hay que seleccionar un tip0 de
datos para el campo. Si este se trata de un campo calculado o un campo de
busqueda y no solo una copia de un campo redefinido para usar un nuevo tipo de
datos, simplemente hay que activar el boton de radio apropiado.
Estas propiedades se pueden usar para leer o cambiar el valor del campo. Para
cambiar el valor de un campo, el conjunto de datos habra de estar en mod0 de
edicion. Otra alternativa a1 uso de las propiedades As, es acceder a1 valor de un
campo usando su propiedad value, que se define como una variante.
La mayoria de las demas propiedades del componente TField, tales como
Alignment, DisplayLabel, Displaywidth y Visible, reflejan ele-
mentos de la interfaz de usuario del campo y las utilizan 10s distintos controles
data-aware, sobre todo DBGrid.
En el ejemplo FieldAcc, haciendo clic sobre el tercer boton de velocidad cam-
bia la alineacion de cada campo:
p r o c e d u r e TForm2.SpeedButton3Click(Sender: TObject);
var
I: Integer;
begin
f o r I : = 0 t o cds.FieldCount - 1 d o
cds. Fields [I] .Alignment : = tacenter;
end;
-
-
+
-
-
-
b
-
-
-
-
-
-
-
-
Un&d Slates of Arnenca NorthArnexa 9363130 249M0 000
A
Figura 13.6. El aspect0 del ejemplo F~eldAccdespues de haber pulsado 10s botones
Center y Format
El codigo del metodo cdscalc Fields (en cada una de las tres versiones)
accede directamente a algunos campos. Se puede hacer asi porque se ha usado el
editor Fields y, automaticamente, se ha encargado de la creacion de las declara-
ciones de campo correspondientes, como se puede ver en este extract0 de la decla-
ration de interfaz del formulario:
type
TCalcForm = c l a s s (TForm)
cds: TClientDataSet;
cdsPopulationDensity: TFloatField;
cdsArea: TFloatField;
cdspopulation: TFloatField;
cdsName: TStringField;
cdscapital: TStringField;
cdscontinent: TStringField;
procedure cdsCalcFields (Dataset: TDataSet) ;
Cada vez que aiiadimos o eliminamos campos en el editor Fields, podemos ver
el efecto inmediato en la cuadricula del formulario (a no ser que la cuadricula
tenga definidos sus propios objetos de columna, en cuyo caso normalmente no se
vera ningun cambio) .
Por supuesto, en tiempo de diseiio no se veran 10s valores de un campo calcu-
lado; solo se encuentran disponibles en tiempo de ejecucion, porque son el resul-
tad0 de la ejecucion del codigo Delphi compilado.
Dado que hemos definido algunos componentes para 10s campos, podemos
usarlos para personalizar algunos elementos visuales de la cuadricula. Por ejem-
plo, para definir un formato de presentacion que aiiada un punto para separar 10s
miles, podemos usar el Object Inspector para cambiar l a propiedad
DisplayFormat de algunos componentes de campo a # # # , # # # , # # # . El
efecto de este cambio en la cuadricula es inmediato en tiempo de diseiio.
NOTA: El formato de presentacion mencionado utiliza la configuration
intemacional de Windows para dar formato a la salida. Cuando Delphi
-- -
traauce
A _ _ 3
el valor .numerlco
-1......
ae este campo_ a- rexto, Ira- coma
l-: 3- -_A- - -.-I _ _ _ J _ . . _
en ta caaena ae
A_--*_ 3-
Despues de trabajar con 10s componentes de tabla y 10s campos, hemos perso-
nalizado el componente DBGrid usando el editor de su propiedad C o l u m n s .
Hemos configurado la columna Popzrlation Density (densidad de poblacion) como
de solo lcctura y su propiedad But tonstyle como cbsEllipsis, para ofrecer
un editor personalizado. Cuando definimos este valor, aparece un pequeiio boton
con tres puntos si el usuario intenta editar la celda de la cuadricula. Al pulsar el
boton sc invoca a1 evento OnEditButtonClick de la DBGrid:
procedure T C a l c F o r m . D B G r i d l E d i t B u t t o n C l i c k ( S e n d e r : TObject);
begin
MessageDlg (Format (
' T h e p o p u l a t i o n d e n s i t y ( 8 . 2 n ) #I3 +
' i s t h e P o p u l a t i o n ( % . O n ) '#I3 +
' d i v i d e d by t h e A r e a (%.On). '#13#13 +
' E d i t these two f i e l d s t o c h a n g e i t . I ,
[cdsPopulationDensity.AsFloat,
cdsPopulation.AsFloat,
cdsArea.AsFloat] ) ,
mtInf ormation, [mbOK] , 0 ) ;
end;
Esto es todo lo que se necesita para que la lista desplegable funcione (vease la
figura 13.9) y tambien para ver el valor del campo de la referencia cruzada en
tiempo de diseiio. Fijese en que no es necesario personalizar la propiedad C o l u m n s
de la cuadricula porque se usa el boton desplegable y el valor de siete filas viene
de manera predefinida. Eso no significa que no podamos usar esta propiedad para
personalizar aun mas estos y otros elementos visuales de la cuadricula a nuestro
gusto.
Figura 13.9. El resultado del ejemplo FieldLookup, con la lista desplegable de la
cuadricula que muestra valores tornados de la tabla de otro conjunto de datos.
l 4 4 w U + - A p 01brNo ICU~NO
l~ak~ate I~hpDate IE~~NO
U1298CN2315 9/1/1995 91111995 EmpU 0011
OrdnNo
U1300 CN 1384 1011/1995 1W111995 EmpW OOB
r-iiE Ill302 CN 1231 16/1/1995 16/1/1995 ErnpU 0052
C&No #13&5 CN 1356 M/1/1995 2Wlt1995 EmpU DO65
pi%r W1309 CN 3615 22/1/1995 2211/I995 ErwW 0094
Sald)ak W1315CN1651 26/1/1995 26/1/1995 EmpU 012'1
112/1995 W1317CN1984 1/2/1995 Emp# 0138
ShpDae #1350 CN 3052 lm995 112/1995 Empll0071
Itundslned, W1355 CN 3 3 3 5/2/1 995 5/2/1995 EmpW 0141
W1860 CN 3615 4/2/1996 <un&lmeb
Emflo
IEmp#0138
- -- --- - - - - - - -
Este codigo funciona, como muestra la figura 13.1 1. pero tiene algunos pro-
blemas. Uno de ellos es que el puntero de registro se desplaza a1 ultimo registro,
por lo que se pierde la posicion anterior en la tabla. Otro problema consiste en que
la interfaz de usuario se refresca muchas veces durante la operation.
Figura 13.11. La salida del programa Total, que muestra 10s salarios totales de 10s
empleados.
Uso de marcadores
Para evitar estos dos problemas, hay que desactivar las actualizaciones y guar-
dar tambien la posicion actual del puntero de registro en una tabla y recuperarla
a1 final. Para ello, podemos usar un marcador de tabla, una variable especial que
guarda la posicion de un registro en una tabla de conjunto de datos. El enfoque
tradicional de Delphi consiste en declarar una variable del tipo de datos
T B o o kmar k e iniciarla mientras se obtiene la posicion actual de la tabla:
var
Bookmark: TBookmark;
begin
Bookmark : = cds.GetBookmark;
A1 final del metodo Act io nTot a lExe cut e, podemos recuperar la posi-
cion y borrar el marcador con las dos sentencias siguientes (dentro de un bloque
final 1 y para asegurarnos de que se libera la memoria del puntero):
cds.GotoBookmark (Bookmark);
cds.FreeBookmark (Bookmark);
Para evitar el otro efecto secundario del programa (vemos 10s registros despla-
zandose mientras la rutina recorre 10s datos), podemos desactivar temporalmente
10s controles visuales conectados con la tabla. La tabla tiene un metodo
Disablecontrols a1 que podemos llamar antes de que se inicie el bucle
while y un metodo Enablecontrols a1 que podemos llamar a1 final, des-
pues de que se recupere el puntero de registro.
NOTA: Hemos escrito este codigo para mostrar un ejemplo de bucle con el
que recorrer el contenido de una tabla, per0 conviene tener en cuenta que
existe una tecnica alternativa basada en el uso de una consulta SQL, que
devuelve la suma de valores de un campo. Cuando usamos un servidor
SQL, la ventaja de velocidad de una llakada SQL para calcular el total
puede ser significativa, dado que no es necesario lleiar todos 10s datos de
- . - . - - . - - - -. -- .- .-
cada camDo desde el S e ~ d 0 a1 r ordenador del cliente. El serv~dorsolo
envia al cliente el resultado final. Existe una alternativa mejor cuando se
usa un ClientDataSet, ya que aislar una columna es una de las carac-
teristicas
- - - - - -- - -- m e ofrecen 10s aerenados. Lo
1--------- --- -=--=----. -- m
---e hemos comentado aaui
--------I--
---- es
-- una
solution generics, que deberia funcionar para cualquier conjunto de datos.
Edicion de una columna de tabla
El codigo de la accion de increment0 es similar a1 que ya hemos visto. El
metodo A c t i o n 1 n c r e a s e E x e c u t e tambien recorre la tabla, calculando el
total de 10s sueldos, como hacia el metodo anterior. Aunque solo tiene dos
parametros, esiste una diferencia clave. Cuando aumentamos el sueldo, estamos
cambiando 10s datos de la tabla. Las dos sentencias clave se encuentran dentro del
bucle w h i l e :
w h i l e n o t cds. EOF do
begin
c d s .Edit;
cdsSalary.Value : = Round (cdsSalary.Value * S p i n E d i t l - V a l u e )
/ 100;
Total : = Total + cdssalary-Value;
c d s .Next;
end:
/ / dibujo predefinido
DBGridl.DefaultDrawDataCel1 (OutRect, Column.Field, S t a t e ) ;
Figura 13.12. El programa DrawData muestra una cuadricula que incluye el texto de
un campo de memo y el omnipresente pez de Borland.
La Paz
Guyana IGso~gelor
b? Sodh Arne
Jarna~ca Kmgston North Amer
Memco Cty NorlhAmer
Managua NorlhAmer
Asuncion South A m c d
Figura 13.13. El ejemplo MltGrid tiene un control DBGrid que permite la seleccibn de
varias filas.
Veamos el codigo del ejemplo MltGrid; que se activa a1 hacer clic sobre el
boton para mover el campo N a m e de 10s registros seleccionados a1 cuadro de
lista:
procedure TForml.ButtonlClick(Sender: TObject);
var
I: Integer;
BookmarkList: TBookmarkList;
B o o k m a r k : TBookmarkStr;
begin
// almacena l a p o s i c i o n a c t u a l
B o o k m a r k : = cds.Bookmark;
try
// v a c i a e l c u a d r o d e l i s t a
ListBoxl.Items.Clear;
// o b t i e n e l a s f i l a s s e l e c c i o n a d a s d e l a c u a d r i c u l a
BookmarkList : = DbGridl-SelectedRows;
for I : = 0 to BookmarkList.Count - 1 do
begin
// p a r a c a d a u n o , m u e v e l a t a b l a a e s e r e g i s t r o
cds.Bookmark : = BookmarkList[I];
// a i i a d e e l carnpo name d l c u a d r o d e l i s t a
ListBoxl. Items .Add (cds.FieldByName ( ' N a m e ' ) .Asstring);
end;
finally
// v u e l v e a 1 r e g i s t r o i n i c i a l
cds.Bookmark : = Bookmark;
end;
end;
En cada uno de estos casos, existe una alternativa a la creacion de una excep-
cion que consiste en establecer un valor predefinido. Sin embargo, si un campo
tiene un valor predefinido, es mejor presentarlo, para que el usuario pueda ver
que valor se enviara a la base de datos. Para ello, podemos controlar el evento
After Insert del conjunto de datos, que ocurre inrnediatamente despues de
que se haya creado un nuevo registro (podiamos haber usado tambien el evento
OnNewRecord ):
procedure TForml.cdsAfterInsert(DataSet: TDataSet);
begin
cdsContinent.Va1ue : = 'Asia';
end;
Se llama a este metodo siempre que el usuario hace clic sobre el boton, selec-
ciona un elemento del cuadro combinado o pulsa la tecla Intro mientras que se
encuentra en el cuadro de dialogo:
procedure TForml.ComboNameClick(Sender: TObject);
begin
GetData;
end;
Jamaica
&= 1756943
Por ultimo, el usuario puede modificar 10s valores de 10s controles y hacer clic
sobre el boton Send. El codigo que se va a ejecutar depende de si la operacion es
una actualizacion o una insercion. Podemos saberlo fijandonos en el nombre (aun-
que con este codigo, un nombre erroneo ya no podra modificarse):
p r o c e d u r e TForml.SendData;
begin
// crea una e x c e p c i d n , s i no hay nombre
i f ComboName.Text = " then
r a i s e Exception .Create ( ' I n s e r t t h e name' ) ;
/ / v e r i f i e d s i e l r e g i s t r o ya e s t d e n l a t a b l a
i f cds .Locate ( ' N a m e ' , ComboName .Text, [loCaseInsensitive] )
then
begin
/ / modified e l r e g i s t r o encontrado
cds.Edit;
cdsCapita1.AsString : = E d i t c a p i t a l - T e x t ;
cdsContinent.AsString : = ComboContinent.Text;
cdsArea.AsString : = E d i t A r e a - T e x t ;
cdsPopu1ation.AsString : = EditPopulation.Text;
cds .Post;
end
else
begin
/ / inserta un nuevo registro
cds.InsertRecord ([ComboName.Text, EditCapital.Text,
ComboContinent.Text, EditArea.Text,
EditPopulation.Text1);
// a d a d e a la lista
.
ComboName Items .Add (ComboName.Text)
end;
Antes de enviar 10s datos a la tabla podemos realizar cualquier tipo de prueba
de validacion a 10s valores. En este caso, no tiene mucho sentido controlar 10s
eventos de 10s componentes de la base de datos, porque tenemos control total
sobre el momento en el que se realizan las operaciones de actualizacion o inser-
cion.
Agrupacion y agregados
Ya hemos visto que un ClientDataSet puede tener un indice distinto a partir del
orden en que se guardan 10s datos en el archivo. Una vez que se define un indice,
se pueden agrupar 10s datos de acuerdo con ese indice. En la practica, un grupo
esta definido como una lista de registros consecutivos (segun su indice) para 10s
cuales no cambia el valor del campo indexado. Por ejemplo, si tenemos un indice
para la provincia, todas las direcciones de esa provincia entraran en el mismo
grupo.
Agrupacion
El ejemplo CdsCalcs tiene un componente C l i e n t D a t a S e t que extrae sus
datos del habitual conjunto de datos c o u n t r y . c d s .
El grupo se consigue, junto con la definicion de un indice, a1 especificar un
nivel de agrupacion para el indice:
object ClientDatasetl: TClientDataSet
IndexDefs = <
item
Name = ' Clientda taSetl Index1
Fields = 'Continent'
GroupingLeve = 1
en&
IndexName = 'ClientDataSetlIndexl'
Cuando se activa un grupo, se puede hacer obvio para el usuario si se muestra
la estructura de agrupacion en el control DBGrid, como muestra la figura 13.16.
Todo lo que hay que hacer es controlar el evento O n G e t T e x t para el campo
agrupado (en el ejemplo, el campo C o n t i n e n t ) y mostrar el texto solo si el
registro es el primer0 del grupo:
procedure TForml.ClientDataSetlContinentGetText(Sender: TField;
var Text: String; DisplayText: Boolean) ;
begin
i f gbFirst i n ClientDataSetl .GetGroupState (1) then
Text : = Sender.AsString.
else
Text : = ";
end;
Nicara~a
El S alvadar
Cuba
Jaaica
Un~tedSlates d Amarlca Washmglon
Canada Ollawa
S d h America Paraguay Amion
U~WW Monlevideo
Venezuela Caracas
Peru Lima
hgmfina B uermr Aiies
Guyana Georgetown
Ecuador Quito
Cob& Bwta
Chi 'Sancbju -
8rd Brasika
I ~. -. - - - - - - -- - - - - .--
Definicion de agregados
Otra caracteristica del componente C1i e n t D a t a S e t es el soporte de agre-
-
gados. Un agregado es un valor calculado basandose en varios registros, como la
&ma o el valorpromedio de un campo para toda la tabla o un gripe de registros
(definido mediante la logica de agrupacion ya comentada). Los agregados son
mantenidos, es decir, se vuelven a calcular inmediatamente si cambia uno de 10s
registros. Por ejemplo, se puede mantener automaticamente el total de una factura
mientras el usuario escribe 10s elementos de la factura.
lpd+ Es valores cada vez que &imbia an valor. La agregacih se aprove-
i '
&a de 10s datos delta de1 Cll&ntDataSet.~btjempb, para actualizar
un8 suma c u a n d ~cambi$-uij W p q el Clier)tQa,taSst resta e1 antiguo
v a h del agregadb y aiiade e1nqew valor. S&lo se neceskan dos cAlculos,
-90 aunque d a w m i l c s d i fileen esd a g r e d p . . i o r cste motivo,
las actualizaciones de agrega& son instazit6vleas.
o b j e c t ClientDataSetl: TClientDataSet
Aggregates = <
item
Active = True
AggregateName = ' C o u n t '
Expression = 'COUNT (NAME)'
GroupingLevel = 1
IndexName = ' C l i e n t D a t a S e t l I n d e x 1 0
Visible = False
end
item
Active = True
AggregateName = ' T o t a l P o p u l a t i o n '
Expression = 'SUM (POPULATION) '
Visible = False
end>
AggregatesActive = T r u e
Fijese en que en la ultima linea del anterior fragment0 de codigo se debe acti-
var el soporte para 10s agregados, ademas de activar especificamente cada agre-
gad0 que se quiera usar. Es importante inhabilitar 10s agregados, porque tener
muchos de ellos puede ralentizar un programa.
El enfoque alternativo es usar el editor Fields, escoger la orden N e w F i e l d
desde su menu de metodo abreviado y seleccionar la opcion Aggregate (disponi-
ble, junto con la opcion InternalCalc, solo en un ClientDataSet). Esta es la
definition de un campo agregado:
object ClientDataSetl: TClientDataSet
object ClientDataSetlTotalArea: TAggregateField
FieldName = ' T o t a l A r e a '
ReadOnly = True
Visible = True
Active = True
DisplayFormat = ' # # # , # # # , # # # I
Expression = ' S U M (AREA) '
GroupingLevel = 1
IndexName = ' ClientDa taSetl Index1 '
end
Figura 13.17. La parte inferior del editor Fields de un ClientDataSet muestra 10s
campos agregados.
Para usar agregados simples, hay que escribir algo de codigo, como en el
ejemplo siguiente (el V a l u e del agregado es una variante):
procedure TForml.ButtonlClick(Sender: TObject);
begin
Labell.Caption : =
'Area: ' + ClientDataSetlTotalArea.Disp1ayText +
#13'Population : ' +
FormatFloat ( ' # # # , # # # , # # # I , ClientDataSet1.Aggregates
[l] . v a l u e ) +
# 1 3 ' N u m b e r : ' + IntToStr (ClientDataSetl.Aggregates
[0] . V a l u e ) ;
end ;
Estructuras maestroldetalles
Es habitual que se necesite relacionar tablas que tengan una relacion uno a
muchos. Esto significa que, para un unico registro de la tabla maestra existen
muchos registros detallados en una tabla secundaria. Un ejemplo clasico de esto
es una factura y 10s elementos de la factura; otro es una lista de clientes y 10s
pedidos de cada uno de ellos.
Se trata de situaciones habituales en la programacion de bases de datos, y
Delphi ofrece un soporte explicit0 mediante la estructura maestro/detalles. La
clase TDat a S e t tiene un propiedad Data S our c e para configurar una fuente
de datos maestra. Esta propiedad se usa en un conjunto detallado para conectarse
a1 registro actual del conjunto de datos maestro en combinacion con la propiedad
MasterFields.
En la figura 13.18 se puede ver un ejemplo del formulario principal del progra-
ma MastDet en tiempo de ejecucion. Hemos colocado 10s controles data-aware
relacionados con la tabla maestra en la parte superior del formulario, y una cua-
dricula conectada con la tabla de detalle en la parte inferior. De esta manera, para
cada registro maestro, se puede ver inmediatamente la lista de 10s registros de
detalle conectados (en este caso, todos 10s pedidos vinculados con el cliente ac-
tual). Cada vez que se selecciona un nuevo cliente, la cuadricula que se encuentra
bajo el registro maestro muestra solo 10s pedidos que pertenezcan a ese cliente.
4/12/1989
La arquitectura clientelservidor
Las aplicaciones de bases de datos de 10s capitulos anteriores utilizaban com-
ponentes nativos para acceder a 10s datos almacenados en archivos de un ordena-
dor local y cargar todo el archivo en memoria. Se trata de un enfoque extremo. De
manera mas tradicional, el archivo se lee registro a registro de manera que varias
aplicaciones puedan acceder a el a1 mismo tiempo, usando algunos mecanismos
de sincronizacion en la escritura.
Cuando 10s datos se encuentran en un servidor remoto, copiar toda una tabla
en memoria para procesarla es una tarea que consume tiempo y ancho de banda, y
suele resultar inutil. Como ejemplo, supongamos que tenemos una tabla como
EMPLOYEE (parte de la base de datos de ejemplo de InterBase, incluida con
Delphi) y que le aiiadimos miles de registros y la colocamos en un ordenador en
red como un servidor de archivos.
Si queremos conocer el maximo salario que paga la empresa, podemos abrir un
componente de tabla de dbExpress ( E m p T a b l e ) o una consulta de seleccion de
todos 10s registros y ejecutar este codigo:
EmpTable.Open;
EmpTable-First;
MaxSalary := 0;
while not EmpTable.Eof d o
begin
i f EmpTable. FieldByName ( ' S a l a r y ' ) .Ascurrency > MaxSalary
then
MaxSalary : = EmpTable. FieldByName ( ' S a l a r y ' ) .AsCurrency;
EmpTable .Next;
end;
El efecto de este enfoque es que lleva todos 10s datos de la tabla desde el
ordenador en red a1 ordenador local, una operacion que puede tomar minutos. En
este caso, el enfoque correct0 seria permitir que el servidor SQL calcule directa-
mente el resultado, devolviendo unicamente esta informacion. Puede hacerse esto
si se usa una sentencia SQL como:
select Max (Salary) from Employee
NOTA: Los dos fragmentos de cirdigo anteriores forman parte deI ejernplo
GetMax, que incluye cbdigo para cronometrar las dos tdcnicas. Para utiIizar
el componente Table de la pequefia tabla Employee se necesitan unas
diez veces m h de tiempo que para realizar la consulta, aunque el servidor
InterBase est6 instalado en el ordenador en que se ejecuta el programa.
Entidades y relaciones
La tecnica de diseiio de bases de datos relacionales clasica, basada en el mode-
lo entidad-relacion (E-R), implica tener una tabla para cada entidad que necesite-
mos representar en la base de datos, con un campo para cada elemento de datos
que necesitemos y un campo adicional para cada relacion uno a uno o uno a varios
con otra entidad (o tabla). En el caso de las relaciones varios a varios, sera
necesaria una tabla a parte.
Como ejemplo de relacion uno a uno, supongamos una tabla que represente un
curso universitario. Tendria un campo para cada elemento de datos importante
(nombre y descripcion, sala en la que se impartira, etc.) ademas de un campo
unico en el que se indique el profesor. Los datos del profesor, de hecho, no se
deberian almacenar junto con 10s datos del curso, sin0 en una tabla independiente,
puesto que podria hacerse referencia a ellos desde cualquier otra parte.
El horario de cada curso puede incluir un numero no definido de horas de dias
distintos, por lo que no pueden aiiadirse dentro de la misma tabla que describe el
curso. En su lugar, esta informacion habra de colocarse en una tabla aparte, que
contenga todos 10s horarios, con un campo que haga referencia a la clase corres-
pondiente a cada horario. En una relacion uno a muchos como esta, "muchos"
registros de la tabla de horarios apuntan de nuevo al mismo registro ("uno") de la
tabla de cursos.
Se necesita una situacion mas compleja para almacenar informacion sobre que
estudiante recibira que clase. No se pueden listar 10s estudiantes directamente en
la tabla de cursos, porque su numero no es fijo, y las clases no se pueden guardar
en 10s datos sobre estudiantes por la misma razon. Asi, en una relacion varios a
varios de este tipo, la unica tecnica consiste en tener una tabla adicional que
represente la relacion y liste las referencias a estudiantes y cursos.
Reglas de normalizacion
Los principios clasicos sobre diseiio incluyen una serie de reglas de normaliza-
cion. El objetivo de estas reglas consiste en evitar la duplicacion de datos en las
bases de datos (no solo para ahorrar espacio, sino sobre todo para evitar producir
incoherencias de datos). Por ejemplo, no repetimos todos 10s datos de cliente en
cada pedido, sino que hacemos referencia a una entidad de cliente aparte. Asi,
ahorramos memoria y cuando cambien 10s datos del cliente (corno, por ejemplo,
un cambio de direccion), todos 10s pedidos de este cliente incluiran 10s nuevos
datos. Otras tablas que se relacionen con el mismo cliente tambien se actualizaran
automaticamente.
Las reglas de normalizacion implican el uso de codigos para valores repetidos
comunmente. Por ejemplo, si tenemos diferentes opciones de envio, no utilizare-
mos una descripcion basada en una cadena para dichas opciones en la tabla de
pedidos, sino un codigo numeric0 corto, proyectado sobre una descripcion en una
tabla de busqueda aparte. .
Esta ultima regla, que no deberia llevarse a1 extremo, ayuda a evitar tener que
agrupar un gran numero de tabla para cada consulta. Podemos tener en cuenta la
violacion de algunas de estas reglas de normalizacion en algunos casos (dejando
una breve descripcion del envio en la tabla de pedidos) o usar el programa cliente
para ofrecer la descripcion, con lo que acabariamos de nuevo con un diseiio de
base de datos formalmente incorrecto. Esta ultima opcion resulta practica solo
cuando usamos un entorno de desarrollo unico (corno Delphi) para acceder a la
base de datos.
do, 10s OID son totalmente aleatorios, sin ninguna norma de secuenciacion
y nunca resultan visibles pita el usuario final. Esto significa que podemos
2 - -1
seguir usanao _.._A:L..A-_
claves susc~iulas I_: --_-_-_- _--_L..___L__-I_
(SI nuttscra ttmpresa ttsla acosiumuraaa a
- _ A 1 -
ellas) junto con 10s OID, per0 todas las referencias externas a la tabla se
basarin en 10s OID.
Otra norma comun sugerida por 10s promotores de esta tecnica (que forma
parte de las teorias que apoyan la proyeccion relacional a objetos) es el uso
de identificadores unicos en todo e! sistema. Si tenemos una tabla de empre-
sas clientes v una tabla de em~leados. , .vodriamos . -
ureguntarnos uor auk
deberiamos usar un identificador unico para datos tan diversos. La razon es
que, si lo bacemos asi, podremos vender productos a un empleado sin tener
----A:- 1 - :-r
que r e p e ~ ~ :L- --L--
larmrorrnauon -a - - - I - - - I -
swore GI ernpleauo en -- la
1 - .-LI- 1- -I:--&--
raola ue L--:--
cuenws, nawen-
do sencillamente referencia at empleado en nuestro pedido o factura. AI-
guien identificado mediante un OID realiza un pedido y dicbo OID puede
remitir a varias tablas distintas. Usar identificadores OID y proyeccion
relacional a objetos es un elemento avanzado del disefio de aplicaciones de
bases de datos en Delphi. Es aconsejable investigar mhs acerca de este tema
antes de embarcarse en un proyecto medio o grande de Delphi porque 10s
beneficios pueden ser importantes (despues de una inversion en estudiar
este enfoque y crear un codigo bhico de soporte).
Mas restricciones
Ademas de la exclusividad de las claves primarias y de las restricciones
referenciales, generalmente podemos usar la base de datos para imponer mas
normas de validez para 10s datos. Podemos pedir que columnas concretas (como
las que se refieren a un identificador fiscal o a un numero de pedido de compra)
incluyan so10 valores unicos. Podemos imponer la exclusividad de 10s valores
para varias columnas, por e.jemplo, para indicar que no podemos dar clases en la
misma aula a la misma hora.
Por lo general, se pueden expresar normas sencillas para imponer restricciones
sobre una tabla, mientras que para las normas mas complejas, hay que ejecutar
procedimientos almacenados activados mediante disparadores (cada vez que 10s
datos cambian, por ejemplo, o que hay datos nuevos).
Una vez mas, hay muchos temas relacionados con un correct0 diseiio de bases
de datos. per0 10s elementos comentados en esta seccion serviran para proporcio-
nar un bucn punto de partida.
-
NOTA: Para conseguir mas infonnacion sobre el lenguaje de definicion de
datos (DDL)de SQL y el lenguaje de manipulacibn de datos (DML),
consultense las referencias del anexo D en el CD-ROM.
Cursores unidireccionales
En bases de datos locales, las tablas son archivos secuenciales que tienen un
orden o bien fisico o definido por un indice. Por el contrario, 10s servidores SQL
funcionan en conjuntos de datos logicos, no relacionados mediante un orden fisi-
co. Un servidor de bases de datos relacionales controla datos segun el modelo
relacional, un modelo matematico basado en una teoria fija.
Lo importante en este ambito cs que en una base de datos relacional, 10s regis-
tros (a veces llamados tuplas) de una tabla no se identifican por su posicion sino
exclusivamente mediante una clave primaria. basada en uno o mas canipos. Cuando
hemos obtenido un con.junto de registros, el servidor aiiade a cada uno de ellos
una referencia a1 siguiente, lo cual hace que sea mas rapido ir desde un registro a1
siguiente, per0 muy lento volver al registro anterior. Por esta razon, se suele decir
que un RDBMS usa un cursor unidireccional. Conectar una tabla o consulta de
este tipo a un control DBGrid resulta practicamente imposible, puesto que se
ralentizarian terriblemente las busquedas hacia atras en la cuadricula.
Algunos motores de bases de datos guardan en una cache 10s datos ya conse-
guidos, para soportal- una navegacion completamente bidireccional. En la arqui-
tectura de Delphi. es el componente C 1i e n t D a t a S e t el que se encarga de esta
tarea, o algun otro conjunto dc datos con almacenamicnto local de datos. Veremos
este proccso mas detenidamente mas adelante. cuando nos centremos en dbEspress
y cl componente C l i e n t D a t a S e t .
nombre.
-
mole V w Sewer JP
839 ' * % I - - -- .- -- - -- -
I EMP-NO
FIRST-NAME
LAST NAME
[EMPNO] SMALLINT
[FIRSTNAME] VARCHAR(151
ILASTNAMEI VARCHARlZOl
Yes
IHIRE-DATE
DEPT-NO
JOB CODE
JOB~GRADE
JOB-COUNTRY
TIMESTAMP
[DEPTNO) CHAR01
IJOBCODEI VARCHARISI
~JOBGRADEJSMALLINT.
(COUNTRYNAME] VARCHARIlS]
DEFAULT 'NOW No
No
No
No
No
SALARY [SALARY] NUMERIC[lS. 21 DEFAULT 0 No
FULL-NAME VARCHAR Yes
-- -- - ---
IC \ \exam&s\~atabese\e& adb Tables
Figura 14.2. IBConsole puede abrir ventanas independientes para rnostrar 10s
detalles de cada entidad (en este caso, una tabla).
f r m employee
where salary > l O O O I 2 0
Figura 14.3. La ventana Interactive SQL de IBConsole perrnite probar consultas que
se planee incluir en programas Delphi.
Procedimientos almacenados
Los procedimientos almacenados son como las funciones globales de una uni-
dad Delphi, y deben llamarse explicitamente desde el lado del cliente. Los pro-
cedimientos almacenados suelen utilizarse para definir rutinas para el
mantenimiento de 10s datos, para agrupar secuencias de operaciones necesarias
en distintas situaciones, o para contener complejas sentencias select.
Al igual que 10s procedimientos de Delphi, 10s procedimientos almacenados
pueden tener uno o mas parametros con tipo. Por contra, pueden tener mas de un
valor de retorno. Como alternativa a devolver un valor, un procedimiento almace-
nado tambien puede devolver un conjunto de resultados (el resultado de una sen-
tencia s e 1 e ct interna o un conjunto fabricado de forma personalizada).
El siguiente fragment0 de codigo es un procedimiento almacenado escrito para
InterBase: recibe una fecha como entrada y calcula el salario mas alto entre todos
10s empleados contratados en esa fecha:
create procedure MaxSalOfTheDay (ofday date)
returns (maxsal decimal ( 8 , 2 ) ) as
begin
s e l e c t max (salary)
from employee
where hiredate = :ofday
i n t o :maxsal;
end
Hay que prestar atencion a1 uso de la clausula into, que indica a1 servidor
que guarde el resultado de las sentencia select en el valor de retorno maxsal .
Para modificar o eliminar un procedimiento almacenado, se pueden usar mas
adelante 10s comandos alter procedure y drop procedure.
Si nos fijamos en este procedimiento almacenado, podriamos preguntarnos
cual es la ventaja en comparacion con la ejecucion de una consulta similar activa-
da en el cliente.
La diferencia entre 10s dos enfoques no esta en el resultado conseguido, sin0 en
su velocidad. Un procedimiento almacenado se compila en el servidor en una
notacion intermedia mas rapida durante su creacion, y el servidor escoge en ese
momento la estrategia a utilizar para acceder a 10s datos. En cambio, una consul-
ta se compila cada vez que se envia la peticion a1 servidor. Por este motivo, un
procedimiento almacenado puede sustituir a una consulta muy compleja, siempre
que no cambie muy a menudo.
Desde Delphi, se puede activar un procedimiento almacenado con el siguiente
codigo SQL:
select
from MaxSalOfTheDay ( 'Ol/Ol/ZOO3 ')
Disparadores (y generadores)
Los disparadores se comportan mas o menos como 10s eventos en Delphi, y se
activan automaticamente cuando se produce un evento determinado. Los
disparadores pueden tener codigo especifico o llamar a procedimientos almacena-
dos; en ambos casos, la ejecucion se realiza completamente en el servidor. Los
disparadores se usan para mantener la consistencia de 10s datos, comprobar 10s
datos nuevos de un mod0 mas complejo que el que permite la verificacion de
restricciones, y para automatizar efectos secundarios de algunas operaciones de
entrada (como crear un registro de 10s cambios de sueldo anteriores cuando se
modifica el sueldo actual).
Los disparadores pueden activarse mediante tres operaciones basicas de ac-
tualizacion de datos: i n s e r t , u p d a t e y d e l e t e . Cuando se crea un dispara-
dor, se indica si se deberia lanzar antes o despues de una de estas tres acciones.
Como ejemplo de un disparador, podemos utilizar un generador para crear un
indice unico en una tabla. Muchas tablas utilizan un indice unico como clave
primaria. InterBase no tiene un campo AutoInc (de incremento automatico). Ya
que varios clientes no pueden generar identificadores unicos, se puede delegar en
el servidor para que haga esto. Casi todos 10s servidores SQL ofrecen un contador
a1 que se puede llamar para solicitar un nuevo identificador, que deberia usarse
mas tarde para la tabla. InterBase llama a estos contadores automaticos genera-
dores, mientras que Oracle 10s llama secuencias: Este es el codigo de InterBase de
ejemplo:
c r e a t e generator cust-no-gen;
...
gen-id (cust-no-gen, 1);
Este disparador se define para la tabla de clientes y se activa cada vez que se
inserta un nuevo registro. El simbolo new se refiere a1 nuevo registro que se
introduce. La opcion p o s i t i o n indica el orden de ejecucion de varios
disparadores conectados a1 mismo evento. (Los disparadores con 10s valores mas
bajos se ejecutan en primer lugar.)
Dentro de un disparador, se pueden escribir sentencias DML que actualicen
tambien otras tablas, per0 hay que prestar atencion a las actualizaciones que
acaban volviendo a activar el disparador y crean una recursion sin fin. Despues se
puede modificar o inhabilitar el disparador, mediante una llamada a las senten-
cias a l t e r t r i g g e r o d r o p t r i g g e r .
Los disparadores se activan automaticamente para 10s eventos especificados.
Si hay que hacer muchos cambios en la base de datos utilizando operaciones de
lotes, la presencia de un disparador puede ralentizar el proceso. Si 10s datos de
entrada ya se han comprobado en relacion a su coherencia, se puede desactivar
temporalmente el disparador. Estas operaciones de lotes suelen codificarse en
procedimientos almacenados, per0 en general estos procedimientos no envian sen-
tencias DDL como las necesaria para desactivar y volver a activar el disparador.
En este caso, se puede definir una vista basada en un comando select * f r o m
table,creando asi un pseudonimo para la tabla. Despues se puede permitir que
el procedimiento almacenado realice el procesamiento de lotes sobre la tabla, y
aplique el disparador a la vista (que deberia utilizarse tambien por parte del
programa cliente).
La biblioteca dbExpress
Actualmente. el principal acceso a una base de datos de un servidor SQL en
Delphi lo proporciona la biblioteca dbEspress. Como mencionamos en el capitulo
13, no es la unica posibilidad, per0 es la mas usada. La biblioteca dbExpress se
present6 en Kylix y Delphi6, y permite acceder a varios servidores distintos
(IntcrBase. Oracle, DB2, MySql, Informix y ahora SQL Server de Microsoft).
Hemos ofrecido una vision general de dbExpress en comparacion con otras s o h -
ciones en cl capitulo anterior, asi que aqui nos saltaremos la presentation y nos
ccntraremos en elementos mas tecnicos.
/ / l e c t u r a de l a i n f o r m c i o n de v e r s i o n
nInfoSize : = GetFileVersionInfoSize (pChar(strDriverNarne),
nDetSize) ;
if nInfoSize > 0 then
begin
GetMern (pVInfo, nInfoSize) ;
try
GetFileVersionInfo (pChar(strDriverName), 0,
nInfoSize, pVInf o) ;
VerQueryValue (pVInfo, ' \ ' , pDetail, nDetSize) ;
Result : = HiWord
(TVSFixedFileInfo ( p D e t a i l A ).dwFileVersionMS) ;
finally
FreeMem (pVInfo) ;
end;
end;
end;
El componente SQLConnection
La clase TSQLConnecti o n hereda del componente TCustomConnection y
maneja conesiones a bases de datos, lo mismo que sus clases hermanas (10s com-
ponentes Database, ADOConnection e IBConnection).
Como podemos ver a1 comparar 10s dos listados, este es un subconjunto de 10s
parametros del controlador. Cuando creamos una nueva conexion, el sistema co-
piara 10s parametros predefinidos del controlador. A continuacion, podemos edi-
tarlos para la conexion especifica (proporcionando, por ejemplo, un nombre de
base de datos correcto). Cada conexion se relaciona con el controlador para cada
uno de sus atributos clave, como indica la propiedad DriverName. Hay que
tener en cuenta que la base de datos a que se hace referencia aqui es el resultado
de una edicion, de acuerdo con 10s parametros usados en la mayoria de 10s ejem-
plos. Lo importante es recordar que estos archivos de inicializacion se usan solo
en tiempo de diseiio. Cuando seleccionamos un controlador o una conexion en
tiempo de diseiio, 10s valores de dichos archivos se copian en las propiedades
correspondientes del componente SQLConnec t io n, como en este ejemplo:
object SQLConnectionl: TSQLConnection
ConnectionName = ' IBLocal '
DriverName = ' Interbase'
GetDriverFunc = 'getSQLDriverINTERBASET
LibraryName = ' dbexpint . dll'
Loginprompt = False
Params.Strings = (
' Blobsize=-1 '
'CodtRetain=Palse'
'Database=c:\Archives d e programa \Archives
comunes\Borland Shared\Data\employee.gdb'
' DriverName=Interbasel
' Password=mas terkey'
' RoleName=RoleNamel
' ServerCharSe t = A S C I I 1
'SQLDialect=ll
'Interbase T r a n s I s o l a t i o n = R e a d C o d t e d '
' User-Name=sysdba '
' Wai tOnLocks=Truel)
VendorLib = 'GDS32.DLL'
end
Add T d e lo SOL
- --- --
FIRST-NAME
LAST-NAME
PHONE-EX1
HIRE-DATE
OEPT-NO
JOB CODE
Add Fpld lo SQ1
Cancel 1 ~ d p _I
Figura 14.5. El CommandText Editor usado por el componente SQLDataSet para
consultas.
El componente SQLMonitor
El ultimo componente del grupo dbExpress es SQLMonitor, utilizado para
registrar las solicitudes enviadas desde dbExpress al servidor de bases de datos.
Este componente de seguimiento permite ver las ordenes enviadas a la base de
datos y las respuestas recibidas a bajo nivel, haciendo un seguimiento del trafico
entre cliente y servidor a bajo nivel.
Aplicacion de actualizaciones
En cada ejemplo basado en una cache local, a1 igual que el ofrecido por 10s compo-
nentes C 1 i e n t DataSe t y S i m p l e D a t a S e t , es importante escribir 10s cambios
locales de nuevo en el servidor de la base de datos. Esto normalmente se realiza
llamando a1 metodo A p p l y U p d a t es . Podemos mantener 10s cambios en la cache
local durante algun tiempo y aplicar despues una serie de actualizaciones a la vez
o enviar cada cambio directamente. En estos dos ejemplos, hemos empleado la
ultima tecnica, adjuntado 10s siguientes controladores de eventos a 10s eventos
A f t e r P o s t (que se activa despues de las operaciones de edicion o insercion) y
A f t e r D e l e t e de 10s componentes C l i e n t D a t a S e t :
p r o c e d u r e TForml .Doupdate (Dataset: TDataSet) ;
begin
// a p l i c a i n m e d i a t a m e n t e 10s c a m b i o s l o c a l e s a la b a s e d e
// d a t o s
SQLClientDataSetl.ApplyUpdates(0);
end;
Seguimiento de la conexion
Otra funcion que hemos aiiadido a 10s ejemplos DbxSingle y DbxMulti, es la
capacidad de seguimiento ofrecida por el componente S Q L M o n i t o r . En el ejem-
plo, el componente se activa a1 iniciarse el programa. En el ejemplo DbxSingle,
ya que el S i m p 1e D a t a S e t incluye la conexion, el monitor no puede conectarse
a ella en tiempo de diseiio, sino solo cuando arranque el programa:
p r o c e d u r e TForml.FormCreate(Sender: TObject);
begin
SQLMonitor1.SQLConnection : = SimpleDataSet1.Connection;
SQLMonitorl.Active : = True;
S i m p l e D a t a S e t l - A c t i v e : = True;
end
Cada vez que hay una cadena de seguimiento disponible, el componente activa
el evento O n T r a c e para permitirnos decidir si incluir la cadena en el registro. Si
el parametro L o g T r a c e de dicho evento es T r u e (el valor predefinido), el com-
ponente registra el mensaje en la lista de cadenas T r a c e L i s t y activa el evento
O n L o g T r a c e para indicar que se ha aiiadido una nueva cadena a1 registro.
El componente tambien puede almacenar automaticamente el registro en el
archivo indicado por su propiedad F i l e N a m e , per0 no hemos usado esta fun-
cion en el ejemplo. Todo lo que hemos hecho ha sido controlar el evento
O n L o g T r a c e , copiando todo el registro en el componente de memo mediante el
codigo siguiente (generando la salida que muestra la figura 14.6):
procedure TForml.SQLMonitorlLogTrace(Sender: TObject;
CBInfo: pSQLTRACEDesc; var LogTrace: Boolean) ;
begin
Memol.Lines : = SQLMonitor1.TraceList;
end;
INTERBASE ~sc-cwnnwt-relammg
INTERBASE ~sc-dsql-free-statement
INTERBASE . ISC-start-lransact~on
INTERBASE - ISC-dsql-allocale-statement
#updaleEMPLOYEE set
PHONE-EXT = 7
where
EMP-NO = 9 and
FIRST-NAME = 7 and
LAST-NAME = 7 and
PHONE EXT = 7 and
HIRE-D~TE = ? a i d
-
DEPT-NO = ? a n d
JOB-CODE ? and
JOB-GRADE = ? and
JOB-COUNTRY - ? and
SALARY = 7 and
FULL-NAME = ?
INTERBASE .isc-dsql-prepare
INTERBASE .isc-dsql-sql-inlo
INTERBASE .sc-van-inlegel
INTERBASE - kc-ds@-describe-bimd
INTERBASE - SQLD~alect= 1
TRUCO: Este resultado es mejor que en Delphi 6 (sin aplicar 10s parches),
ya que esta operacibn causaba un error debido a que el campo clave no se
establecia correctamente.
Si queremos tener mas control sobre como se generan las sentencias de actua-
lizacion, necesitamos trabajar con 10s campos del conjunto de datos subyacente,
que estan disponibles tambien cuando se usa el componente aglutinador
SimpleDataSet (que tiene dos editores de campos, uno para el componente basico
ClientDataSet del que hereda y otro para el componente SQLDataSet que
incluye).
Hemos corregido de un mod0 parecido el ejemplo DbxMulti, despues de aiiadir
campos permanente para el componente SQLDataSet y modificar las opciones
del proveedor para incluir algunos de 10s campos en la clave o escluirlos de las
actualizaciones.
Figura 14.7. El ejemplo SchemaTest permite ver las tablas de una base de datos y las
columnas de una tabla dada.
'All shown
Este codigo muestra 10s cmpleados del pais seleccionado cn la DBGrid, tal y
como muestra la figura 14.9.
Como alternativa al uso de 10s elementos de la matriz Params por posicion,
podria considerarse el uso del metodo ParamByName;para evitar cualquier tipo
dc problema en caso de que la consulta se acabe modificando y 10s parametros
adoptcn un orden distinto.
-
England
I
EMP-NO IFIRST-NAMEIWT-NAME IPHONE-EXT HIRE-DAVE IDEPT-NO(J~
k 28 Ann Eennel 5 2/1/1991 120 Ad
Para controlar la impresion, hemos escrito una rutina en cierto mod0 generica,
que requiere como parametros 10s datos que se van a imprimir, una barra de
progreso para la informacion de estado, la fuente de salida y el tamaiio de formato
maximo de cada campo. Toda la rutina usa el soporte de impresion de archivo y
da formato a cada campo con una cadena de tamaiio fijo, alineada a la izquierda
para producir un tipo de informe en columna. La llamada a la funcion Format
posee una cadena de formato parametrica creada de forma dinamica usando el
tamaiio del campo.
En el listado 14.1 se puede ver el codigo del metodo PrintOutDataSet
principal, que utiliza tres bloques try/ fina 11y anidados para liberar todos
10s recursos del mod0 correcto:
Listado 14.1. El rnetodo principal del ejemplo UniPrint.
// d e f i n e l a f u e n t e y m a n t i e n e l a o r i g i n a l
oldFont : = TFontRecall-Create (Printer.Canvas.Font);
try
Printer.Canvas.Font : = Font;
try
data.Open;
try
// imprime el encabezamiento (nombres d e campo) en
// negrita
Printer.Canvas.Font.Sty1e : = [fsBold];
f o r I : = 0 t o data. Fieldcount - 1 do
begin
sizeStr : = IntToStr (min
(data.Fields [i] .Displaywidth, maxSize) ) ;
Write (PrintFile, Format ( ' B - ' + sizeStr + 's',
[data.Fields [i] . FieldName] ) ) ;
end;
Writeln (PrintFile);
El programa recurre a esta rutina cuando se hace clic sobre el boton Print All.
El programa ejecuta una consulta independiente (select count ( * ) f r o m
EMPLOYEE), que devuelve el numero de registros de la tabla de empleados. Esta
consulta es necesaria para preparar la barra de progreso (el conjunto de datos
unidireccional, en realidad, no tiene forma alguna de conocer el numero de regis-
tros que va a recuperar hasta que ha alcanzado el ultimo). A continuacion, define
la fuente de la salida, usando posiblemente una fuente con un ancho fijo, y llama
a la rutina PrintOutDataSet:
p r o c e d u r e TNavigator.PrintAllButtonClick(Sender: TObject);
var
Font: TFont;
begin
// d e f i n e e l r a n g o d e l a P r o g r e s s B a r
EmplCountData.Open;
try
ProgressBarl .Max : = EmplCountData. Fields [0] .AsInteger;
finally
Emp1CountData.Close;
end;
Manipulacion de actualizaciones
Una de las ideas principales quc esta tras el componente C l i e n t D a t a S e t
es que se utiliza como una cache local para obtener la entrada de un usuario y. a
continuacion, enviar un lote de solicitudes de actualizacion a la base de datos. El
componente posee tanto una lista de 10s cambios que se van a aplicar al servidor
de la base de datos, almacenada en el mismo formato usado por el
C l i e n t D a t a S e t (accesiblc a travcs de la propiedad D e l t a ) , como un com-
pleto registro de actualizaciones que podemos manipular con algunos metodos
(incluyendo la capacidad de deshacer 10s cambios).
- . -
JDEPT-NO(EMP-NO
[FIRST-NAME [LAST-NAME IPHDWE-~~T~~ALWY
usUnmodlied 115 118 Takash Yamamlo 23
usUnmod111ed 125 121 Roberto Ferran 1
100 127 Mihael Yanawski 432
123 134 Jacques Glon 937
623 136 colt Johnson 265
usUnmod~fied 621 138 T.J Green 218
urUnmodilied 672 144 John Montgomery 820
usModrfied 622 145 Mark Guckenhr 931
uslnserled 622 146 John Rohd 932
Acceso a Delta
Mas alla de examinar el estado de cada registro, el mejor mod0 de entender que
cambios han sucedido en un ClientDataSet dado (pero no se han cargado aun a1
scrvidor) consiste en fijarse en el delta, la lista de cambios que esperan ser aplica-
dos a1 servidor. Esta propiedad se define como sigue:
property Delta: Olevariant;
En el ejemplo CdsDelta, hemos aiiadido un modulo de datos con 10s dos com-
ponentes C l i e n t Da t a S e t y una fuente de datos: un SQLDataSet proyectado
sobre la tabla EMPLOYEE de muestra de InterBase. Ambos con.juntos de datos
de cliente tienen el campo calculado adicional de estado (status), con una version
ligeramente mas gentrica que el codigo comentado antes, porque el controlador
de eventos es compartido entre ambos.
- -
- wModtfd 91 -
- usUmd~hed
usD&ed ? 21 141 Pmre Osba~ne llOOW
- 7 23 134 Jacques Gbn 390W
-usModltnj 937
A
Figura 14.1 1. El ejemplo CdsDelta permite ver las solicitudes de actualizacion temporal
almacenadas en la propiedad Delta del ClientDataSet.
- . - - - - .- -- - ~ .
-- - - -- - --
-R d *
c Skip
Cbnd
C Coned
C Rdmh
C Mape
. -- -
Figura 14.12. El dialogo Reconcile Error que ofrece Delphi en el Object Repository y
que usa el ejemplo CdsDelta.
Uso de transacciones
Si trabajamos con un servidor SQL, deberiamos usar transacciones para que
nuestras aplicaciones fbese mas robustas. Podemos pensar en una transaccion
como una serie de operaciones que se consideraran como un todo unico, "atomi-
cow,que no se puede dividir.
Un ejemplo puede ayudar a aclarar la idea. Supongamos que tenemos que
aumentar el sueldo de cada empleado de una empresa un tanto por ciento dado,
como en el ejemplo Total del capitulo 13. Un programa tipico ejecutaria una serie
de sentencias SQL en el servidor, una para cada registro que necesite ser actuali-
zado. Si se produjera un error en esa operacion, podriamos deshacer 10s cambios
anteriores. Si se considera la operacion "aumentar el sueldo de cada empleado"
como una unica transaccion, deberia realizarse completamente o ignorarse com-
pletamente. 0, podemos considerar la analogia con las transacciones bancarias:
si un error provoca que solo se realice parte de la operacion, podriamos acabar
con dinero de menos o de mas.
Trabajar con operaciones de bases de datos como transacciones resulta muy
util. Se puede comenzar una transaccion y realizar varias operaciones que debe-
rian considerarse todas ellas como parte de una operacion mayor. Entonces, a1
final, se pueden confirmar 10s cambios o deshacer la transaccion, descartando
todas las operaciones realizadas hasta el momento. Lo mas tipico es que se quiera
deshacer una transaccion si se produce un error en alguna de sus operaciones.
Existe otro punto que merece la pena resaltar: las transacciones sirven tambien
durante la lectura de datos. Hasta que 10s datos Sean confirmados por una tran-
saccion, otras conexiones y/o transacciones no deberian verlos. Una vez que se
hayan confirmado 10s datos procedentes de una transaccion, otras deberian ver el
cambio cuando lean 10s datos, es decir, a menos que se necesite abrir una transac-
cion y volver a leer 10s mismos datos para realizar un analisis de 10s mismos o
complejas operaciones de generacion de informes. Distintos servidores SQL per-
miten leer datos en una transaccion de acuerdo con alguna de o todas estas alter-
nativas, como veremos cuando analicemos 10s niveles de aislamiento de
transacciones.
Es muy sencillo controlar las transacciones en Delphi. Por defecto, cada ope-
ration de edicion y rnodificacion se considera como una transaccion implicita
unica, per0 podemos modificar este comportamiento controlando las operaciones
explicitamente. Simplemente usamos 10s siguientes tres metodos del componente
SQLConnection de dbExpress (otros componentes de conexion de bases de datos
tienen metodos similares):
StartTransaction: Marca el comienzo de la transaccion.
Commit: Confirma todas las actualizaciones de la base de datos realiza-
das durante la transaccion.
Rollback: Devuelve la base de datos a su estado anterior a la transaccion.
Podemos utilizar tambien la propiedad In T r a n s a c t i o n para comprobar si
una transaccion esta activada. Lo mas normal es usar un bloque t r y para desha-
cer una transaccion cuando se lanza una excepcion, o se puede confirmar la tran-
saccion como ultima operacion del bloque t r y , que se ejecutara solo cuando no
se produzcan errores. El codigo podria tener este aspecto:
var
TD: TTransactionDesc;
begin
TD.TransactionID : = 1;
TD.IsolationLeve1 := xilREADCOMMITTED;
SQLConnectionl.StartTransaction(TD);
t rY
/ / - - l a s o p e r a c i o n e s d e la t r a n s a c c i o n v a n a q u i - - -
SQLConnectionl .Commit (TD);
except
SQLConnectionl .Rollback (TD);
end;
p r o c e d u r e TForml.FormCreate(Sender: TObject);
var
Reg: TRegistry;
begin
Reg : = TRegistry.Create;
try
Reg.RootKey : = HKEY-LOCAL-MACHINE;
Reg.OpenKey('\Software\Borldnd\Borland
S h a r e d \ C u r r e n t V e r s i o n l , False) ;
1BDatabasel.DatabaseName : =
Reg. Readstring ( ' R o ot D i r e c t o r y l) +
'exarnples\da t a b a s e \ e m p l o y e e . g d b l;
finally
Reg. Free;
end ;
EmpDS.DataSet.Open;
end;
212850 0
Si ejecutamos 10s dos ejemplos a1 mismo tiempo, la salida del programa IbxMon
mostrara en una lista 10s detalles de la interaccion del programa IbxUpdSql con
InterBase, como muestra la figura 14.15.
_~ai:l
Slat~ibcsI S a w Froperk: I Utao 1
b 4753PM
[Appbcabon Ibxupdsql]
IBDalabasel [Connect]
6 47 53 PM
[Applrca~onIbxupdsql]
IBTransacllonl [Stad lransact~on]
6 47 53 PM
[Appi catton Ibxupdsql]
IBDataSell Prepae] SELECT Employee EMP-NO, Ewloyee FIRST-NAME. EmployeeLAST-NAME,
Department DEPARTMENT Job JOB-T ITLE, Employee SALARY Employee DEPT-NO Employee JOB-CODE
EmployeeJ 00-GRADE, Employee JOB-COUNTRY
FROM EMPLOYEE Employee
INNER JOlN DEPARTMENT Department
ON (Oepaltment DEPT-NO = EmployeeDEPT-NO)
INNER JOlN JOB Job
-
ON @&JOB-CODE EmployeeJOB-CODE]
AND [JobJOB-GRADE = EmployeeJOB-GRADE]
AND [JobJOB-COUNTRY = Employee JOB-COUNTRY]
ORDER BY Depa~tmentDEPARTMENT
Pbm PLAN SORT [JOIN [EMPLOYEE NATURALJOB INDEX [RDBSFRlMARY2),DEPARTMENT INDEX [RDB
PRIMARYS)]]
6 47 53 PM
[Appl~catlon Ibmpdsqfl
IBDataSetl [Prepare] delete lrom EMPLOYEE
whera
.
EMP-NO OLD-EMF-NO
11
Figura 14.15. La sahda del ejemplo IbxMon, basada en el componente lBMa
/ / o b t i e n e 10s d a t o s d e l u s u a r i o
1BSecurityServicel.DisplayUsers;
// muestra el nombre d e cada u s u a r i o
f o r i : = 0 t o IBSecurityServicel.UserInfoCount - 1 do
w i t h IBSecurityServicel.UserInfoli] do
RichEdit4 .Lines .Add (Format ( ' U s e r : % s , F u l l N a m e : % s ,
Id: %dl,
[UserName, FirstName + ' ' + LastName, UserId] ) ) ;
Generadores e identificadores
Como hemos dicho antes, somos partidarios del uso de identificadores para
identificar 10s registros de cada tabla de una base de datos.
- -- - --
.-
BpplyEvenl - -- I
On New Recud I
r OnPost I
c r e a t e t r i g g e r companies-bu f o r companies
a c t i v e before update p o s i t i o n 0
as
begin
i f (new.name <> old.name) then
new. name-uppe r = upper (new.name) ;
end;
Por ultimo, hemos aiiadido un indice a la tabla con esta sentencia DDL:
create index i-companies-name-upper on companies(name-upper) ;
Con esta estructura interna, podemos seleccionar todas las empresas que
comiencen con el texto del cuadro de edicion (edsearch) escribiendo el siguiente
codigo en una aplicacion Delphi:
dm.DataCompanies.Close;
dm.DataCompanies.Se1ectSQL.Text :=
' s e l e c t c . i d , c.name, c. tax-code,' +
' from companies c ' +
' w h e r e name-upper s t a r t i n g w i t h "' +
Uppercase (edsearch.Text) + ' ' ' ' ;
dm.DataCompanies.0pen;
1 TRUCO: Usando una consulta preparada con p a r b e t r o s , podriamos ha- I
I cer que el codigo fuese a h msJ rapido.
I
Como alternativa, se podria crear un campo calculado de servidor en la defini-
cion de tabla, per0 hacer esto impediria tener un indice en el campo, que acelera
las consultas en gran medida:
name-upper varchar (50) computed by (upper (name))
c r e a t e t a b l e people
(
id d-uid n o t n u l l ,
id-company d-uid n o t n u l l ,
id-location d-uid n o t n u l l ,
name v a r c h a r (50) n o t n u l l ,
phone v a r c h a r ( 15 ) ,
fax v a r c h a r ( 15 ) ,
email v a r c h a r (50) ,
key-contact d-boolean,
c o n s t r a i n t people-pk primary key (id),
c o n s t r a i n t people-uc unique (id-company, name)
) ;
c r e a t e t r i g g e r people-ai f o r people
active a f t e r i n s e r t position 0
as
begin
/ * s i una persona e s e l c o n t a c t o c l a v e , elimina
e l i n d i c a d o r de todas l a s d e d s ( d e l a misma empresa) * /
if (new.key-contact = ' T' ) t h e n
u p d a t e people
set key-contact = ' P I
where id-company = new.id-company
and id <> new.id;
end;
c r e a t e t r i g g e r people-au f o r people
a c t i v e a f t e r update p o s i t i o n 0
as
begin
/ * si una persona es el contacto clave, elimina el
indicador de todas las d e d s (de la misma empresa) * /
if (new.key-contact = ' T' and old.key-contact = ' F' ) then
update people
set key-contact = ' F '
where id-company = new.id-company
and id <> new.id;
end;
procedure TDmCompanies.DataPeopleAfterInsert(DataSet:
TDataSet) ;
begin
/ / inicializa 10s datos del registro detalle
// con una referencia a1 registro maestro
DataPeopleID-COMPANY.AsInteger := DataCornpaniesID.As1nteger;
/ / la ubicacion sugerida es la activa, si estd disponible
if not DataLocations.IsEmpty then
DataPeopleID-LOCATION-AsInteger : = DataLocationsID.As1nteger;
// la primera persona que se afiade se transform en el
contacto clave
// (verifica si el conjunto de datos de personas filtrado
// estd vacio)
DataPeopleKEY-C0NTACT.AsBoolean : = DataPeople.IsErnpty;
end;
ILI
ID I~D-COMP~ID-LOCPTIONIKEY_CONTACT~WE 1 ~ ~ 6 9 1 ~
g
I.;
I 13 3 11 T Chuck J
14 9 11 F David l I
-
rn
El formulario se encuentra dentro de un formulario principal, que a su vez esta
basado en un control de pagina, que incluye otros formularios. Solo el formulario
creado con la primera pagina se crea durante el arranque del programa. El metodo
ShowForm que hemos escrito se encarga de que el formulario sea "adoptado"
por la hoja con solapa del control de ficha, despues de eliminar el borde del
formulario:
procedure TFormMain.FormCreate(Sender: TObject);
begin
ShortDateFormat : = 'dd/m/yyyyr;
ShowForm (TFormCompanies.Create (Self), TabCompanies) ;
end;
Reserva de clases
Parte del programa y de la base de datos esta relacionada con la reserva de
clases y cursos de formacion (aunque este programa se creo como demostracion,
tambien resulta practico.) En la base de datos existe una tabla con clases que lista
todos 10s cursos de formacion, cada uno con un titulo y una fecha fijados. Otra
tabla contiene la matricula por empresa, incluyendo las clases matriculadas, el
identificador de la empresa y algunas anotaciones. Por ultimo, una tercera tabla
contiene una lista de personas que se han apuntado, cada una de ellas conectada a
una matricula para su empresa con la cantidad pagada.
El razonamiento que se esconde tras este proceso de matriculacion basado en
empresas es que las facturas se envian a las empresas, que reservan las clases
para sus programadores y pueden recibir descuentos especificos. En este caso, la
base de datos esta un poco mas normalizada, puesto que la matricula de personas
no se refiere directamente a una clase, sino solo a la matricula de la empresa para
dicha clase. Veamos la definicion de las tablas correspondientes (se omiten las
restricciones de claves externas y otros elementos):
create table classes
(
id d-uid not null,
description varchar ( 5 0 ),
starts-on timestamp not null,
constraint classes-pk primary key ( i d )
) :
create table classes-reg
I
\
El modulo de datos para este grupo de tablas utiliza una relacion maestro1
detalleldetalle, y posee codigo para establecer la conexion con el registro maestro
activo cuando se crea un nuevo registro de detalle. Cada conjunto de datos posee
un campo generador para su identificador y cada uno tiene las sentencias SQL
update e insert apropiadas.
Dichas sentencias se han generado mediante el editor de componente corres-
pondiente usando solo el campo identificador para identificar 10s registros exis-
tentes y actualizar solo 10s campos de la tabla original. Cada uno de 10s dos
conjuntos de datos secundarios obtiene datos de una tabla de busqueda (ya sea la
lista de empresas o la lista de personas). Por ultimo, tuvimos que editar manual-
mente las sentencias Ref reshSQL para repetir la union interna adecuada. Vea-
mos un ejemplo:
object IBClassReg: TIBDataSet
Database = DmMain.IBDatabase1
Transaction = IBTransactionl
AfterInsert = IBClassRegAfterInsert
DeleteSQL-Strings = (
'delete from classes-reg'
'where id = :old-id')
InsertSQL. Strings = (
'insert into classes-reg (id, id-class, id-company, notes) '
' values (:id, :id-class, :id-company, :notes) ' )
RefreshSQL. Strings = (
' select reg. id, reg.id-class, reg.id-company, reg.notes,
c.name '
'from classes-reg reg'
'join companies c on reg.id-company = c.id'
'where id = :id' )
SelectSQL. Strings = (
'select reg. id, reg.id-class, reg.id-company, reg.notes,
c.name '
' from classes-reg reg'
'join companies c on reg.id-company = c.id'
'where id-class = :id1)
ModifySQL-Strings = (
' update classes-reg'
'set'
' id = :id,'
' id-class = :id-class,'
' id-company = :id-company,'
' notes = :notes1
'where id = :old-id' )
GeneratorField.Field = 'id'
GeneratorFie1d.Generator = 'g-master'
Datasource = dsclasses
end
dd
L ID
-
19 Bc~lmdCorp
23 Wlr$echItaha Srl
21 NAME
Davd l
24 Chuck J
AMOUNT300
3W
-
--
// p r e p a r a e l SOL p a r a l a s t r e s s o l a p a s
SqlComrnands : = TStringList.Create;
SqlCommands .Add ( ' w h e r e S t a r t s - O n > ' ' n o w " ' ) ;
SqlCommands.Add ( ' w h e r e S t a r t s - O n <= " n o w " and ' +
' e x t r a c t ( y e a r f r o m S t a r t s- O n ) >= e x t r a c t ( y e a r f r o m
current- t i m e stamp) ' ) ;
SqlCommands.Add ( ' w h e r e e x t r a c t ( y e a r f r o m S t a r t s - O n ) < ' +
' e x t r a c t ( y e a r from current-times tamp) ' ) ;
end;
p r o c e d u r e TFormClasses.TabChange(Sender: TObject);
begin
dm.IBC1asses.Active : = False;
dm.IBClasses.Se1ectSQL [I] : = SqlCommands [Tab.TabIndex];
dm.IBC1asses.Active : = True;
end;
Podemos ver este tercer formulario del programa RWBlocks en la figura 14.20.
Por supuesto que no sugerimos que se afiada edicion SQL a 10s programas pensa-
dos para todos 10s usuarios. Esta caracteristica basicamente se destina a usuarios
avanzados o programadores.
.....".__-... , , , , , , , -,
selecl ' lrom classes
--
Figura 14.20. El formulario de consulta libre del ejernplo RWBlocks esta pensado
para usuarios avanzados.
Trabajo
con ADO
Desde mediados de 10s aiios 80, 10s programadores de bases de datos han
buscado el "santo grial" de la independencia de las bases de datos. La idea es
utilizar una API unica que puedan usar las aplicaciones para interactuar con
muchas fuentes de datos distintas. El uso de una API de este tip0 liberaria a 10s
desarrolladores de la dependencia con respecto a un unico motor de bases de
datos y les permitiria adaptarse a las necesidades en constante carnbio de todo el
mundo.
Los fabricantes han producido muchas soluciones para este objetivo, siendo
las dos mas notables Open Database Connectivity (ODBC) de Microsoft y la
Integrated Database Application Programming Interface (IDAPI) de Borland,
que es mas conocida como Borland Database Engine (BDE).
Microsoft comenzo a sustituir ODBC con OLE DB a mediados de 10s aiios 90,
con el exito de COM. Sin embargo, OLE DB es lo que lo que Microsoft clasifica-
ria como una interfaz a nivel de sistema y esta pensada para 10s programadores a
nivel de sistema. Es una solucion muy grande, compleja y exquisita. Requiere un
programador habil y capaz y un alto grado de conocimiento, a carnbio de una
productividad muy baja. ActiveX Data Objects (ADO) es una capa que recubre a
OLE DB y suele definirse como una interfaz a nivel de aplicacion. Es considera-
blemente mas simple que OLE DB y mas relajada. En pocas palabras, esta dise-
iiada para 10s programadores de aplicaciones.
Como se trato en el capitulo 14; Borland tambien ha sustituido a BDE por una
tecnologia mas reciente. llamada dbExpress. ADO tiene mas parecidos con BDE
que con la tecnologia ligera de dbExpress. BDE y ADO soportan la navegacion y
manipulacion de conjuntos de datos, el procesamiento de transacciones y las ac-
tualizaciones en cache (llamadas actualizaciones por lotes en ADO). de manera
que 10s conceptos relacionados con el uso de ADO son similares a 10s de BDE.
The Delphi Magazine y para otros temas no relacionados con Delphi. Tam-
bien ha participado en numerosas conferencias en Norte America y en Eu-
ropa. Guy vive en Inglaterra con su mujer, su hijo y su gato.
Proveedores de OLE DB
Los proveedores de OLE DB permiten acceder a una fuente de datos. Se trata
de 10s homologos de ADO a 10s controladores dbExpress y 10s SQL Links de
BDE. Cuando se instala MDAC, se instalan automaticamente 10s proveedores
OLE DB que muestra la tabla 15.1
Un ejemplo practico
Ya basta de teoria: veamos como funcionan las cosas. Colocamos un ADOTable
en un formulario. Para indicar la base de datos a la que conectarse, ADO usa
cadenas de conexion. Se puede escribir una cadena de conexion manualmente si
se sabe lo que se esta haciendo. En general, se usara un editor de cadenas de
conexion (el editor de propiedad para la propiedad C o n n e c t i o n s t r i n g ) , que
muestra la figura 15.1.
Este editor aiiade poca cosa al proceso de escribir una cadena de conexion, por
lo que se puede hacer clic sobre el boton Build para usar directamente el editor de
cadenas de conesion de Microsoft, que se muestra en la figura 15.2.
Se trata de una herramienta que es importante conocer. La primera pestaiia
muestra 10s proveedores OLE DB y de servicio instalados en el ordenador. La
lista variara segun la version de MDAC y de otro software instalado. En este
ejemplo, seleccionaremos el proveedor OLE DB Jet 4.0. Hacemos doble clic so-
bre Jet 4.0 OLE Dl3 Provider y aparecera la pestafia Connection. Esta pagina
varia segun el proveedor seleccionado; para Jet, solicita el nombre de la base de
datos y 10s detalles del usuario de conexion. Se puede acceder a un archivo MDB
de Access instalado por Borland junto con Delphi 7: el archivo dbdemos.mdb
disponible en la carpeta compartida de datos (de manera predefinida, C:\Archivos
de programa\Archivos comunes\Borland Shared\Data\dbdemos.mdb). Haremos
clic sobre el boton Probar Conexi6n para dar validez a las opciones selecciona-
das. La pestafia Avanzadas maneja el control de acceso a la base de datos; aqui
se especifica el acceso exclusivo o de solo lectura a la base de datos. La pestaiia
Todas muestra una lista de todos 10s parametros de la cadena de conexion. La
lista es especifica del proveedor OLE DB que se haya escogido al principio.
(Deberiamos fijarnos con atencion en esta pagina, ya que contiene muchos
parametros que son la respuesta a muchos problemas.) Despues de cerrar el editor
de cadenas de conexion de Microsoft, ser vera en el editor de Borland para la
propiedad C o n n e c t i o n s t r i n g el valor devuelto a esta propiedad (que aqui
se muestra en varias lineas por comodidad):
Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=C:\Archivos d e programa\Archivos comunes\Borland
Shared\Data\dbdemos.mdb;
Persist Security Info=False
Las cadenas de conexion son simples cadenas con muchos parametros separa-
dos por punto y coma. Para aiiadir, modificar o eliminar cualquiera de estos
parametros mediante programacion, hay que escribir rutinas propias para encon-
trar el parametro en la lista y modificarlo como corresponda. Un enfoque mas
simple es copiar la cadena en una lista de cadenas de Delphi y usar su prestacion
de pares nombrelvalor: esta tecnica se mostrara en el ejemplo JetText que vere-
mos mas adelante.
I*, !
Mtcrosolt OLE D8 Prov~de~for O~acle
Mlcrosolt OLE D8 Prowder for Outlook Search
Mlcrosolt OLE DB Pronder for SOL Serva
Mlcrosoft OLE DB S~mpleP ~ o w d e ~
MSDataShape
,
Propiedades dinamicas
Imaginemos que somos 10s responsables del diseiio de una nueva arquitectura
de software intermedio (middleware) de bases de datos. Tenemos que reconciliar
10s dos objetivos antagonicos de conseguir una sola API para todas las bases de
datos y acceder a funciones especificas de todas las bases de datos. ADO tiene
que resolver estos objetivos que a1 parecer se excluyen mutuamente, y lo hace
utilizando propiedades dinamicas. Casi todas las interfaces ADO, y sus corres-
pondientes componentes dbGo, tienen una propiedad denominada p r o p e r t i e s
que es un conjunto de propiedades especificas de cada base de datos. Se puede
acceder a estas propiedades mediante su posicion ordinal, del siguiente modo:
ShowMessage (ADOTable1.Properties [I] .Value) ;
Las propiedades dinamicas dependen del tip0 de objeto y tambien de 10s pro-
veedores OLE DB. Para hacernos una idea de su importancia, una conexion ADO
tipica o un conjunto de registros tiene aproximadamente 100 propiedades dinami-
cas. Como se vera a lo largo de este capitulo, las respuestas a muchas preguntas
sobre ADO tienen que ver con las propiedades dinamicas.
TRUCO: Un evento importante relacionado con el uso de propiedades di-
n M c a s es OnRecordsetCreate, que se introdujo en una actualiza-
cion de Delphi 6 y est&disponible en Delphi7. OnRecordse tcreate se
usa inmediatamente despuCs de que se haya creado un conjunto de regis-
tros, pero antes de que se haya abierto. Esto resulta util para definir algu-
nas propiedades d i n h i c a s puesto que algunas de ellas s6lo se pueden definir
cuando el conjunto de registro estA cerrado.
Cada campo de una clave primaria posee una fila unica en el conjunto de
resultados. Asi, una tabla con una clave compuesta por dos campos tiene dos
filas. Los dos valores EmptyParam indican que dichos parametros estan vacios
y se ignoran. El resultado de este codigo se muestra en la figura 15.3, despues de
ajustar el tamaiio de la cuadricula con algo de codigo personalizado.
Cuando se pasa EmptyParam como segundo parametro, el conjunto de re-
sultados incluye toda la informacion del tipo solicitado para toda la base de datos.
Para muchos tipos de informacion, querremos filtrar el conjunto de resultado. Por
supuesto que podemos aplicar un filtro tradicional de Delphi a1 conjunto de resul-
tad0 usando las propiedades F i l t e r y F i l t e r e d o el evento O n F i l t e r -
R e c o r d . Sin embargo, esto aplica el filtro en la parte de cliente de este ejemplo.
Usando el segundo parametro, podemos aplicar un filtro mas eficaz en la fuente
de la informacion esquematica. El filtro se especifica como una matriz de valores.
Cada elemento de la matriz posee un significado especifico importante para el
tipo de datos que se van a devolver. Por ejemplo, la matriz de filtro para claves
primarias posee tres elementos: El primer0 es el catalogo (catalogo es el tkrmino
ANSI para base de datos), el segundo es el esquema, y el tercero es el nombre de
la tabla. Este ejemplo devuelve una lista de claves primarias para la tabla Customer:
var
Filter: OLEVariant;
begin
Filter : = VarArrayCreate ( [0, 2 1 , varvariant) ;
Filter [ Z ] : = 'CUSTOMER';
ADOConnectionl.OpenScherna(
siPrimaryKeys, Filter, Emptyparam, ADODataSetl);
end;
country Name
customer CustNo
emdo~ee EmpNo
tlems ItemNo
ems 01derNo
MSysAccessOblecls 10
orders OrhNo
parts Pa~tNo
vendas VendorNo
Figura 15.3. El ejemplo Openschema obtiene las claves primarias de las tablas de la
base de datos.
- -- - -- -- --
- - - - - - ----- - -
-
N,- de paede dbtener @ xqi~ma$nfoma&m ufilizanda-ADQX, ADOX
9 ecnologia A ~ adicianal
O qpepemite obtener y actualizm m&ma-
ci6n esi$uematica. Es el ,egu'rv@en'te en N O a1 lenguaje de desdi&~de
de SQL (DofaD e f i n i t i ~ n L n n ~ o g e , !con
~ L las
) , sentenciasCREWI2'~.
mTEIR, DROP. y al lenguaje t.!C ,eor#roI da-datos (DafaCo~ltrdlh?guage,
#Kt;) . GRANT y REVOKE. dbGo no soporta directamente ADOX, per6
con
puede importarse la biblioteca de tipos ADOX y usarla con exito en aplica-.
universal como OpenScherna, asi que hay muchos huews sin cubrir. Pafa
simplemente obtener la information esquemhtica y no actualizarla,
Openschema suele ser una opcion mejor.
N n b e _ - - - --
Data Souce
I I
Figura 15.4. Definicion de propiedades extendidas,
Por desgracia para 10s usuarios de Paradox, en algunos casos tendremos que
instalar el BDE ademas del motor Jet. Jet 4.0 necesita el BDE para poder actuali-
zar tablas Paradox, per0 no para solo leerlas. Lo mismo sucede en el caso de la
mayoria de las versiones de Paradox ODBC Driver. Microsoft ha recibido criti-
cas muy justificadas por este tema y ha creado un nuevo Paradox IISAM que no
necesita el BDE. Se pueden conseguir estos controladores actualizados gracias al
Servicio Tecnico de Microsoft.
-
Dent + 55 41 338.5031
Ford Prelecl 141I 9957 0293
Marvin Robot (41 1 232.91 98
Tr~ll~an + 55 41 282 2399
BecMebrm 273-3522
A1
Figura 15.5. ABCCompany.xls en Delphi.
Todas las sentencias de exportacion siguen estas mismas normas basicas, aun-
que algunos controladores IISAM tienen distintas interpretaciones de lo que es
una base de datos. En este caso, exportaremos 10s mismos datos a Excel:
ADOConnectionl .Execute ( S E L E C T * I N T O C u s t o m e r I N " ' +
CurrentFolder + ' d b d e m o s . x l s " " E x c e l 8 . 0;" PROM CUSTOMER' ) ;
Ubicacion de cu.rsor
La propiedad c u r s o r l o c a t i o n permite especificar quien controla la recu-
peracion y actualizacion de datos. Existen dos opciones: el cliente ( c l u s e c l i e n t )
o el servidor ( c l u s e se r v e r ) . La decision tomada afectara a la funcionalidad
del conjunto de datos, asi como a su rendimiento y escalabilidad.
Un cursor de cliente lo administra el motor ADO Cursor Engine. Este motor es
un excelente ejemplo de un proveedor de servicio OLE DB: Presta servicio a otros
proveedores OLE DB. El motor ADO Cursor Engine administra 10s datos del
cliente de la aplicacion. Todos 10s datos del conjunto de resultado se consiguen
del servidor cuando se abre el conjunto de datos. Por tanto, 10s datos se mantienen
en memoria y el motor ADO Cursor Engine se encarga de las actualizaciones y la
manipulacion. es algo parecido a usar el componente ClientDataSet en una apli-
cacion dbExpress. Una ventaja de esta manipulacion de 10s datos, despues de la
recuperacion inicial, consiste en su considerable velocidad incrementada. Aun
mas, dado que la manipulacion se realiza en memoria, el motor ADO Cursor
Engine resulta mas versatil que la mayoria de 10s cursores de servidor y ofrece
servicios adicionales. Mas adelante analizaremos estas ventajas, a1 igual que otras
tecnologias de cursores de cliente (corno 10s conjuntos de registros desconectados
y permanentes).
Un cursor de servidor lo administra el RDBMS. En una arquitectura clientel
senidor fundarnentada en una base de datos como SQL Server, Oracle o InterBase,
esto significa que el cursor se administra fisicamente en el servidor. En una base
de datos de escritorio como Access o Paradox, la ubicacion del "servidor" es solo
una ubicacion logica, puesto que la base de datos se ejecuta en el escritorio. Los
cursores de servidor normalmente son mas rapidos de cargar que 10s cursores de
cliente porque no se transfieren todos 10s datos al cliente cuando se abre el con-
junto de datos. Gracias a esto, son mas apropiados para conjuntos de resultados
muy grandes en 10s que el cliente no tenga memoria suficiente para mantener todo
el conjunto de resultados en memoria. Con frecuencia, podemos decidir que tipos
de caracteristicas estaran disponibles con cada ubicacion de cursor al pensar en el
mod0 de funcionamiento del cursor. Un buen ejemplo del mod0 en que sus carac-
teristicas nos ayudan a decidir el tip0 de cursos es el cierre o bloqueo. Para
colocar un cierre sobre un registro hace falta un cursor de servidor, porque debe
haber una comunicacion entre la aplicacion y el RDBMS.
Otro aspect0 que afectara a la eleccion de la ubicacion del cursor es la
escalabilidad. Los cursores de servidor son administrados por el RDBMS. En una
base de datos cliente/senidor, este estara ubicado en el servidor. A medida que
aumente el numero de usuarios de la aplicacion, la carga del senidor aumenta con
cada cursor de senidor. Una mayor carga en el senidor significa que el RDBMS
se convierte en un cuello de botella cada vez mas rapido, por lo que la aplicacion
resulta menos escalable. Podemos mejorar la escalabilidad utilizando cursores de
cliente. El impact0 inicial a1 abrir el cursor es normalmente mas fuerte, porque
todos 10s datos se transfieren a1 cliente, per0 el mantenimiento del cursor abierto
puede ser menor. Como puede comprobarse, hay muchas cuestiones conflictivas
relacionadas con la eleccion de la ubicacion del cursor apropiada para 10s conjun-
tos de datos.
Tipo de cursor
La eleccion de la ubicacion del cursor afecta directamente a la eleccion dcl tip0
de cursor. Existen cuatro tipos de cursores que podemos usar para cualquier fin o
proposito, per0 hay un valor que no se puede usar, porque es un valor "sin espe-
cificar". Muchos valores en ADO implican un valor sin especificar, y 10s analiza-
remos y explicaremos porque no hay que utilizarlos. Existen en Delphi solo porque
existen en ADO. ADO se diseiio basicamente para programadores en Visual Basic
y C. En estos lenguajes, se pueden usar directamente objetos sin la ayuda que
proporciona dbGo. De ese modo, se pueden crear y abrir conjuntos de registros,
tal y como se llaman en terminos de ADO, sin tener que especificar cada valor
para cada propiedad. Las propiedades para las que no se ha especificado un
valor, tienen un valor no especificado. Sin embargo, en dbGo se utilizan compo-
nentes. Estos componentes tienen constructores, y esos constructores dan un va-
lor inicial a cada propiedad de 10s componentes. De ese modo, desde el momento
en que se crea un componente dbGo, generalmente se tendra un valor para cada
propiedad. como consecuencia, no hay mucha necesidad de tener valores sin espe-
cificar en muchos tipos enumerados.
Los tipos de cursor afectan al mod0 en que se leen y actualizan 10s datos.
Existen cuatro opciones: solo de avance, estatico, conjunto de claves y dinamico.
Antes de profundizar mas en todas las combinaciones de ubicaciones de cursor y
tipos de cursor, deberia quedar claro que solo hay disponible un tip0 de cursor
para 10s cursores de cliente: el cursor estatico. Todos 10s demas tipos de cursor
son unicamente para cursores de servidor. Hablaremos mas adelante sobre la
disponibilidad de 10s tipos de cursor despues de comentar 10s distintos tipos de
cursor, en orden creciente de coste:
Cursor s61o de avance: Este tipo de cursor es el menos costoso y, por lo
tanto, el tipo con el mejor rendimiento posible. Como su nombre indica,
permite desplazarse hacia delante. El cursor lee el numero de registros
especificados por Cachesi ze (predefinido como 1) y cada vez que se
queda sin registros, lee otro conjunto de CacheSi ze registros. Cualquier
intento de desplazarnos hacia atras por el conjunto de resultados mas alla
del numero de registros que hay en cache creara un error. Este comporta-
miento es parecido a1 de un conjunto de datos dbExpress. Un cursor solo
de avance no resulta apropiado para se utilizado en la interfaz de usuario
en la que el usuario puede controlar la direccion a traves del conjunto de
resultados. Sin embargo, si resulta apropiado para las operaciones por
lotes, 10s informes y las aplicaciones Web sin estado, porque dichas situa-
ciones comienzan por la parte superior del conjunto de resultados y fimcio-
nan de forma progresiva hasta el final, cerrando a continuacion el conjunto
de resultados.
Cursor estatico: Un cursor estatico funciona leyendo el conjunto de resul-
tad0 completo y ofreciendo una ventana de registros Caches i ze en el
conjunto de resultado. Dado el servidor ha recuperado el conjunto de resul-
tad0 completo, podemos desplazarnos adelante y atras a traves del conjun-
to de resultados. Sin embargo, a cambio de esta facilidad, 10s datos son
estaticos, es decir, las actualizaciones, inserciones y eliminaciones realiza-
das por otros usuarios no se pueden ver porque ya se han leido 10s datos del
cursor.
Cursor de conjunto de claves: Un cursor conjunto de claves se compren-
de mejor si separamos el termino en dos palabras: conjunto y clave. Clave,
en este contexto, se refiere a un identificador para cada fila. Normalmente
se tratara de una clave primaria. Por ello, un cursor de conjunto de claves
es un conjunto de claves. Cuando se abre el conjunto de resultados, se lee
la lista completa de claves para el conjunto de resultados. Si, por ejemplo,
el conjunto de datos fuese una consulta como SELECT * FROM CUSTOMER,
la lista de claves se crearia a partir de SELECT CUSTID FROM CUSTOMER.
Este conjunto de claves se mantiene hasta que se cierra el cursor. Cuando
la aplicacion solicita datos, el proveedor OLE DB lee las filas que utilizan
las claves del conjunto de claves. Como consecuencia, 10s datos siempre
estan actualizados. Si otro usuario cambia una fila del conjunto de resulta-
dos, entonces 10s cambios se veran cuando se lean de nuevo 10s datos. Sin
embargo, el conjunto de claves, por si mismo, es estatico. se lee solo cuan-
do el conjunto de resultados se abre a1 principio. Por tanto, si otro usuario
aiiade registros nuevos, dichas adiciones no se veran. Los registros elimi-
nados resultan inaccesibles y 10s cambios en las claves primarias (algo que
no deberia permitirse a 10s usuarios) tambien lo son.
Cursor dinamico: El ultimo tip0 de cursor y el mas costoso es el dinami-
co. Un cursor dinamico es casi identico a un cursor de conjunto de claves.
La unica diferencia es que el conjunto de claves se vuelve a leer cuando la
aplicacion solicita datos que no estan en cache. Ya que, de manera prede-
terminada TADOData Set .Caches ize es 1,dicha solicitud resulta muy
frecuente. Puede imaginarse la carga adicional que esto supone para el
RDBMS y la red, y es el motivo de que este sea el cursor mas costoso. Sin
embargo, el conjunto de resultados puede ver y responder a las adiciones y
eliminaciones realizadas por otros usuarios.
Pedir y no recibir
Tras haberlo explicado todo sobre las ubicaciones y tipos del cursor, debemos
de hacer una advertencia: no todas las combinaciones de ubicacion y tipo de
cursor son posibles. Normalmente, esta es una limitacion impuesta por el RDBMS
o el proveedor OLE DB como resultado de la funcionalidad y arquitectura de la
base de datos. Por ejemplo, 10s cursores de cliente siempre condicionan que el
tip0 de cursor sea estatico. Podemos comprobarlo por nosotros mismos. Aiiadi-
mos un componente ADODataSet a un formulario, definimos su propiedad
Connectionstring como cualquier base de datos, definimos ClientLoca-
tion como clUseCursor y CursorType como ctDynamic.Ahora defi-
nimos Active como True y vigilamos el CursorType:cambia a ctstatic.
A partir de este ejemplo, sacamos la siguiente conclusion:
lo que pedimos no es necesariamente lo mismo que recibimos. Hay que com-
probar siempre las propiedades despues de abrir un conjunto de datos para ver el
efecto real de las solicitudes. Cada proveedor OLE DB realizara distintos cam-
bios de acuerdo con distintas solicitudes y distintas circunstancias, per0 para
tener una idea de lo que podemos esperar, pondremos algunos ejemplos:
El proveedor Jet 4.0 OLE DB cambia la mayoria de 10s tipos de cursor a
conjunto de claves.
El proveedor SQL Server OLE DB cambia normalmente el conjunto de
claves y estatico a dinamico.
El proveedor Oracle OLE DB cambia todos 10s tipos de cursor a solo de
avance.
El proveedor ODBC OLE DB realiza diversos cambios segun el controla-
dor ODBC en uso.
Indices de cliente
Una de las muchas ventajas de 10s cursores de cliente es la capacidad de crear
indices locales o de cliente. Para verlo, podemos suponer que tenemos un conjun-
to de datos de cliente ADO para la tabla Customer de DBDemos, que tiene una
cuadricula conectada, y definir la propiedad Index Fie ldNames del conjunto
de datos como CompanyName.La cuadricula mostrara inmediatamente que 10s
datos estan ordenados por nombre de empresa (CompanyName). Debemos acla-
rar algo importante: para indexar 10s datos, ADO no vuelve a leer 10s datos desde
su fuente. El indice se creo a partir de 10s datos en memoria. Esto significa que no
solo la creacion del indice es tan rapida como seria posible, sin0 que la red y el
RDBMS no se sobrecargan debido a la transferencia de 10s mismos datos una y
otra vez con distintas ordenaciones.
La propiedad IndexFieldNames posee mas potencial. Si la definimos como
Country; CompanyName, veremos 10s datos se ordenan primero por pais y
despues, dentro de cada pais, por nombre de empresa. Ahora, definimos
IndexFieldNames como CompanyName DESC.Debemos asegurarnos que
escribimos DESC en mayusculas y no "desc" ni "Desc". Seguro que no resulta
sorprendente que 10s datos se ordenen de manera descendente.
Esta caracteristica tan simple per0 potente permite resolver algunos de 10s
grandes problemas de 10s desarrolladores de bases de datos. Los usuarios podrian
pedir algo muy razonable e inevitable, si se podria hacer clic sobre las columnas
de la cuadricula para ordenar 10s datos. Las respuestas como sustituir las
cuadriculas por controles que no Sean sensibles a 10s datos como ListView que
tengan incluida la capacidad de ordenacion, o como capturar el evento
O n T i t l e C l i c k del componente DBGrid y rehacer la sentencia SQL SELECT
tras incluir una clausula ORDER BY apropiada no son nada satisfactorias.
Si se tienen 10s datos en la cache del cliente (corno ya se ha visto durante el uso
del componente ClientDataSet), se puede usar un indice de cliente calculado en
memoria. Podemos ariadir el siguiente evento OnT it leC1i c k a la cuadricula:
procedure TForml.DBGridlTitleClick(Column: TColumn);
begin
if ADODataSetl.IndexFieldNames = Column.Field.FieldName then
ADODataSet1.IndexFieldNames : = Column.Field.FieldName + '
DESC'
else
ADODataSetl.IndexFie1dNames : = Column.Field.Fie1dName
end;
Replicacion
ADO tiene una gran cantidad de potentes caracteristicas. Se puede pensar que
eso supone un gran consumo de recursos, per0 tambien se traduce en aplicaciones
mas potentes y fiables. Una de estas caracteristicas es la replicacion o clonacion.
Un conjunto de registros clonado es un nuevo conjunto de registros que posee las
mismas propiedades que el original a partir del que se ha clonado. Vamos a ver en
primer lugar como crear un clon y despuks veremos porqud son tan utiles.
- +
Figura 15.6. El forrnulario del ejernplo DataClone, con dos copias de un conjunto de
datos (el original y el clon).
Procesamiento de transacciones
Como ya vimos en el capitulo 14, el procesamiento de transacciones permite a
10s desarrolladorcs agrupar actualizaciones individuales de una base de datos en
una unidad logica de trabajo unica.
El soporte de procesamiento de transacciones de ADO se controla con el com-
ponente ADOConnection, empleando 10s metodos B e g i n T r a n s , C o r n r n i t T r a n s
y R o l l b a c k T r a n s , que tienen efectos parecidos a 10s de 10s nietodos BDE y
dbExpress correspondientes. Para investigar el soporte del procesamiento de tran-
sacciones de ADO. crearemos un programa de prueba sencillo, llamado
TransProcessing. El programa ticne un componente ADOConnection con su pro-
piedad c o n n e c t i o n s t r i n g configurada para el proveedor Jet 4.0 OLE DB y
el archivo dbdemos.mdb. Tiene un componente A D O T a b l e conectado a la tabla
Customer y un Datasource y una DBGrid para mostrar 10s datos. Por ultimo,
tiene tres botones para ejecutar cada una de las siguientes ordenes:
Transacciones anidadas
Mediante el programa TransProcessing vamos a hacer esta prueba:
1 . Iniciar una transaccion.
2. Cambiar el ContactName del registro Around The Horn de Thomas
Hardy a Dick Solomon.
3. Iniciar una transaccion anidada.
4. Cambiar el ContactName del registro Bottom-Dollar Markets de
Elizabeth Lincoln a Sally Solomon.
5. Deshacer la transaccion mas interna.
6. Confirmar la transaccion externa.
El efecto global es que so10 es permanente el cambio del registro Around The
Horn. Sin embargo, si la transaccion interna se habia confirmado y la transaccion
externa se deshace, el efecto global seria que ninguno de 10s cambios seria perma-
nente (ni siquiera 10s cambios de la transaccion interna). Esto es lo esperado,
siendo el unico limite que Access solo soporta cinco niveles de transacciones
anidadas.
ODBC ni siquiera soporta transacciones anidadas, el proveedor de Jet OLE
DB solo soporta hasta cinco niveles de transacciones anidadas y el proveedor
SQL Server OLE DB no soporta en absoluto la anidacion. Podriamos obtener un
resultado distinto segun la version de SQL Server o del controlador, per0 la docu-
mcntacion y nuestros experimentos con 10s servidores indican que asi sucede.
Aparentemente, solo la transaccion mas externa decide si el trabajo se confirma o
se deshace.
Atributos de ADOConnection
Existe otro tema que deberiamos considerar si estamos pensando en utilizar
transacciones anidadas. El componente ADOConnection posee una propiedad lla-
mada Attributes que determina el mod0 en que se deberia comportar una
transaccion cuando se confirma o se deshace. Se trata de un conjunto de
TXActAtt ributes que, por defecto, esta vacio. Solo hay dos valores en
TXActAttribuf:es:xaCommitRetainingyxaAbortRetaining(este
valor se suele escribir, incorrectamente, como xaRol lbac kRetaining por-
que este seria un nombre mas logico). Cuando se incluye xaComitRetaining
en Attributes y se confirma una transaccion, se inicia automaticamente una
nueva transaccion. Cuando se incluye xaAbortRe taining en Attributes
y se deshace una transaccion, se inicia automaticamente una nueva transaccion.
Esto significa que si incluimos estos valores en Attributes,siempre habra
una transaccion en marcha, porque cuando finalizamos una transaccion siempre
se inicia otra nueva.
La mayoria de 10s programadores prefiere tener un mayor control sobre sus
transacciones y no permitirles que se inicien automaticamente, por lo que estos
valores no suelen usarse. Sin embargo, poseen especial importancia en relacion
con las transacciones anidadas. Si anidamos una transaccion y definimos
Attributes como [xaComitRetaining, xaAbortRetaining1,la
transaccion externa nunca se puede finalizar. Veamos esta secuencia de eventos:
1. Se inicia una transaccion esterna.
2. Se inicia una transaccion interna.
3. La transaccion interna se confirma o deshace
4. Se inicia automaticamente una nueva transaccion interna como consecuen-
cia de la propiedad Attributes.
La transaccion externa no acaba nunca porque cuando una interna finaliza se
iniciara una nueva transaccion. Como conclusion, el uso de la propiedad
Attributes y el uso de transacciones anidadas deberian resultar mutuamente
exclusivos.
Tipos de bloqueo
ADO soporta cuatro tecnicas diferentes para bloquear 10s datos frente a actua-
lizaciones. Las cuatro tecnicas se pueden utilizar mediante la propiedad LockType
del conjunto de datos, con 10s valores: 1tReadOnl y, 1tPessimist ic,
1tOptimistic o ItBatchOptimistic. (Existe ademas una opcion
itunspecified,pero, como ya se comento, ignoraremos 10s valores no espe-
cificados.) En esta seccion ofreceremos una vision global de estos cuatro enfo-
ques. El valor 1tReadOnly especifica que 10s datos son solo de lectura y no
pueden actualizarse. Asi, no se necesita ningun control de bloqueo porque 10s
datos no se pueden actualizar.
Los valores 1tPessimistic y 1topt imistic ofrecen el mismo control
de bloqueo "pesimista" y "optimista" que ofrece el BDE. Una ventaja importante
que ofrece ADO en oposicion a BDE a este respecto es que la opcion del control
de bloqueo es nuestra, en lugar de del controlador BDE. Si se usa una base de
datos de escritorio como dBase o Paradox, el controlador BDE usara un bloqueo
pesimista; si se usa una base de datos clientelservidor como InterBase, SQL Server
u Oracle, el controlador usara el bloqueo optimista.
El bloqueo pesimista
Las palabras pesimista y optimista en este context0 se refieren a lo que espera
el desarrollador de cara a1 conflicto entre actualizaciones de usuario. El bloqueo
pesimista supone que existe una alta probabilidad de que 10s usuarios intenten
actualizar 10s mismos registros a1 mismo tiempo y por lo tanto el conflicto es
probable. Para evitar dicho conflicto, se bloquea el registro cuando comienza la
edicion. El bloqueo se mantiene hasta que se ha finalizado o cancelado la actuali-
zacion. Un segundo usuario que intente editar el mismo registro a1 mismo tiempo
no podra colocar su bloqueo de registro y recibira una excepcion "Could not
update; currently locked" ("No se puedo actualizar, en este momento esta cerra-
do"). Esta tecnica de bloqueo resultara familiar a 10s desarrolladores que hayan
trabajado con bases de datos como dBase y Paradox. La ventaja es que si el
usuario sabe que si puede comenzar a editar un registro, podra guardar su actua-
lizacion con exito. El inconveniente del bloqueo pesimista es que el usuario con-
trola cuando se coloca y se elimina el bloqueo. Si el usuario domina la aplicacion,
el bloqueo podria durar meros segundos. Sin embargo, de cara a una base de
datos, un par de segundos puede ser una eternidad. Por otra parte, el usuario
podria comenzar la edicion e irse a almorzar, y el registro permaneceria bloquea-
do todo ese tiempo, hasta su vuelta. Como consecuencia, la mayoria de 10s defen-
sores del bloqueo pesimista se protegen contra este caso usando un temporizador
u otro dispositivo para provocar la caducidad de 10s bloqueos despues de un
cierto plazo de inactividad a la entrada.
Otro problema del bloqueo pesimista es que necesita un cursor de servidor.
Antes mencionamos que las ubicaciones del cursor influian sobre la disponibili-
dad de 10s diferentes tipos de cursor. Ahora podemos ver que las ubicaciones del
cursor tambien influyen sobre 10s tipos de bloqueo. Mas adelante, analizaremos
las ventajas de 10s cursores de cliente, si se decide aprovechar las ventajas de este
tipo de cursores, no se podra utilizar el bloqueo pesimista.
El bloqueo pesimista es un area de dbGo que ha cambiado en Delphi 6 (respec-
to a Delphi 5). En este apartado se describe como funciona el bloqueo pesimista
de las versiones 6 y 7. Para remarcar este mod0 de funcionamiento, hemos creado
el ejemplo PessimisticLocking. Es parecido a otros ejemplos de este capitulo,
per0 la propiedad CursorLocation se configura como cluseserver y la
propiedad Lo ckType como 1tP e ssimistic.Para usarlo, hay que ejecutar
dos copias desde el Explorador de Windows y tratar de editar el mismo registro en
ambas instancias en ejecucion del programa: no se podra, porque el registro esta-
ra bloqueado por otro usuario.
Esta sentencia proporciona una lista de pedidos y 10s datos de 10s clientes que
han realizado dichos pedidos. El BDE considera que cualquier union SQL es de
solo lectura porque insertar, actualizar y eliminar filas en una union resulta ambi-
guo. Por ejemplo, podriamos plantearnos si la insercion de una fila en la union
anterior originaria un nuevo pedido y tambien un nuevo cliente o solo un nuevo
pedido. La arquitectura ClientDataSet/Provider permite especificar una tabla de
actualizacion principal (y otras caracteristicas avanzadas que no vamos a comen-
tar), y tambien personalizar el codigo SQL de las actualizaciones, como se vio en
parte en el capitulo 14 y se comentara aun mas en el capitulo 16.
ADO soporta un equivalente a las actualizaciones mediante cache, denomina-
do actualizaciones por lotes, que son muy similares a1 enfoque del BDE. En el
proximo apartado hablaremos sobre estas actualizaciones por lotes, lo que puede
ofrecer y por que son tan importantes. Sin embargo, en esta seccion no las necesi-
taremos para resolver el problema de la actualizacion de una union porque, en
ADO, las uniones son intrinsecamente actualizables.
El ejemplo JoinData se basa en un componente A D O D a t a S e t que usa la
union SQL anterior. Si se ejecuta, se puede editar uno de 10s campos y guardar 10s
cambios (saliendo del registro). No se produce ningun error, porque la actualiza-
cion se habra aplicado con exito. ADO, en comparacion con el BDE, ha adoptado
una tecnica mas practica para el problema. En una union ADO, cada objeto de
campo sabe a que tabla subyacente pertenece. Si actualizamos un campo de la
tabla Orders y enviamos el cambio, se crea entonces una sentencia SQL UPDATE
para actualizar el campo de la tabla O r d e r s . Si cambiamos un campo de la tabla
Orders y un campo de la tabla Customer, se crean dos sentencias SQL UPDATE,
una para cada tabla.
La insercion de una fila en una union sigue un comportamiento similar. Si
insertamos una fila e escribimos valores solo para la tabla O r d e r s , se crea una
sentencia SQL I N S E R T para la tabla O r d e r s . Si escribimos valores para am-
bas tablas, se crean dos sentencias SQL I N S E R T , una por tabla. El orden en el
que se ejecutan las sentencias es importante, porque el nuevo pedido podria estar
relacionado con el nuevo cliente, por lo que el nuevo cliente se inserta en primer
lugar .
El mayor problema de la solucion de ADO se puede ver cuando se elimina una
fila de una union. El intento de eliminacion parece no tener exito. El mensaje
exacto que veamos dependera de la version de ADO que estemos usando y de la
base de datos, per0 se nos comunicara que no podemos eliminar la fila porque
otros registros estan relacionados con ella. El mensaje de error puede resultar
confuso. En este caso, el mensaje de error implica que un pedido no puede elimi-
narse porque esisten registros que estan relacionados con el pedido, per0 el error
ocurre tanto si el pedido tiene asociados otros registros como si no. La explica-
cion puede obtenerse siguiendo la misma logica para las eliminaciones que para
las inserciones. Se crean dos sentencias SQL DELETE:una para la tabla Customer
y, a continuacion, otra para la tabla Orders. En contra de lo que pudiera parecer,
la sentencia DELETE de la tabla Orders tiene esito. Es la sentencia DELETE de
la tabla Customer la que no funciona, porque no se puede eliminar el cliente
mientras registros dependientes.
A pesar de entender como funciona este proceso, una forma mejor de enfocar
el problema es desde la perspectiva del usuario. Desde su punto de vista, cuando
se elimina una fila de la cuadricula, casi seguro que el 99 por ciento de 10s
usuarios espera eliminar el pedido, no el pedido y el cliente. Afortunadamente,
podemos conseguir esactamente esto mediante otra propiedad dinamica, en este
caso, la propiedad dinamica Unique Table. Podemos especificar que las eli-
minaciones se refieren solo a la tabla Orders y no a Customer mediante el
siguiente codigo:
A D O Q u e r y l . P r o p e r t i e s [ ' U n i q u e T a b l e ' ] .Value := ' Products' ;
Bloqueo optimista
Ya hablamos anteriormente de la propiedad Loc kType y vimos como funcio-
na el bloqueo pesimista. A continuacion hablaremos del bloqueo optimista, no
solo por tratarse del tip0 de bloqueo preferido para transacciones con un trafico
medio o alto, sino tambien por ser el esquema de bloqueo empleado para las
actualizaciones por lotes.
El bloqueo optimista supone que existe una probabilidad muy reducida de que
10s usuarios intenten actualizar 10s mismos registros a1 mismo tiempo y, por ello,
no es probable que ocurra un conflicto. Asi, la actitud es que todos 10s usuarios
pueden editar cualquier registro en cualquier momento y tratamos con las conse-
cuencias de conflictos entre actualizaciones de diferentes usuarios sobre 10s mis-
mos registros cuando se guardan 10s cambios. De este modo, 10s conflictos se
consideran una excepcion a la norma. Esto significa que no existen controles que
eviten que dos usuarios editen el mismo registro a1 mismo tiempo. El primer
usuario en guardar 10s cambios tendra exito. El intento del segundo usuario de
actualizar el mismo registro puede que no lo tenga. Este comportamiento es esen-
cia1 para las aplicaciones de maletin y aplicaciones Web, en las que no existe una
conexion permanente con la base de datos y, por lo tanto, no hay forma de
implementar un bloqueo pesimista. En oposicion a1 bloqueo pesimista, el bloqueo
optimista posee la considerable ventaja adicional de que 10s recursos solo se
consumen momentaneamente y, por lo tanto, el uso medio de 10s recursos es
mucho menor, haciendo que la base de datos resulte mas escalable.
Vearnos un ejemplo. Supongamos que tenemos un ADODataSet conectado a la
tabla Customer de la base de datos dbdemos .mdb, con LockType definido
como 1tBat chOpt imi s t i c y que el contenido se muestra en una cuadricula.
Supongamos que tambien tenemos un boton para llamar a UpdateBat ch.Eje-
cutamos el programa dos veces (es el ejemplo BatchUpdates) y empezamos a
editar un registro en la primera copia del programa. Aunque por motivos de
sencillez, mostraremos un conflicto empleando un unico equipo, la situacion y
eventos subsecuentes no cambian cuando se usan varios equipos:
1. Escogemos la empresa Bottom-Dollar Markets de Canada y cambiamos el
nombre por Bottom-Franc Markets.
2. Guardamos el cambio, salimos del registro para enviarlo y hacemos clic
sobre el boton para actualizar el lote.
3. Ahora, en la segunda copia del programa, buscamos el mismo registro y
cambiamos el nombre de la empresa a Bottom-Pound Markets.
4. Salimos del registro y hacemos clic sobre el boton para actualizar el lote.
No funcionara.
A1 igual que con muchos otros mensajes de error de ADO, el mensaje exacto
que se reciba dependera no solo de la version de ADO sin0 tambien de la precision
con que se siga el ejemplo. En ADO 2.6, el mensaje de error es "Row cannot be
located for updating. Some values may have been changed since it was last
react' ("No se puede encontrar la fila para su actualizacion. Puede que algunos
valores hayan cambiado desde su ultima lectura"). Este es el comportamiento del
bloqueo optimista. La actualizacion del registro se realiza ejecutando la siguiente
sentencia SQL:
UPDATE CUSTOMER SET CompanyName="Bottom-Pound Markets"
WHERE CustomerID="BOTTM" AND CompanyName="Bottom-Dollar
Markets"
El numero de registros afectados por esta sentencia de actualizacion se espera
que sea uno, porque se busca el registro original utilizando la clave primaria y el
contenido del campo CompanyName tal como estaba cuando el registro se ley6
por primera vez. En este ejemplo, sin embargo, el numero de registros afectados
por la sentencia UPDATE es cero. Esto solo puede ocurrir si se ha eliminado el
registro, ha cambiado la clave primaria del registro o el campo que estamos modi-
ficando fue modificado por otra persona. Por lo tanto, la actualizacion no se
realiza.
Si nuestro "segundo usuario" hubiera cambiado el campo ContactName y
no el campo CompanyName,la sentencia UPDATE se habria parecido a esta:
UPDATE CUSTOMER SET ContactName="Liz Lincoln"
WHERE CustomerID="BOTTM" AND ContactName="Elizabeth Lincoln"
Esta funcion tambien esta disponible para el BDE y otras tecnologias de bases
de datos cambiando a1 uso de componentes Client Dataset, per0 la belleza
de la solucion de ADO esta en que podemos crear toda la aplicacion utilizando
componentes de conjuntos de datos dbGo y no percatarnos de 10s conjuntos de
registros desconectados. En el momento en que descubramos esta caracteristica y
deseemos beneficiarnos de ella, podemos continuar utilizando 10s mismos compo-
nentes que hemos usado siempre. Existen dos razones por las que podriamos
querer desconectar 10s conjuntos de registros:
Para que el numero total de conexiones sea reducido.
Para crear una aplicacion de maletin.
La mayoria de las aplicaciones empresariales clientetservidor abren tablas y
mantienen una conexion permanente con su base de datos mientras la tabla esta
abierta. Sin embargo, normalmente solo existen dos razones por las que queramos
estar conectados a la base de datos: recuperar datos y actualizarlos. Supongamos
que queremos cambiar la tipica aplicacion clientetservidor para que una vez que
se abra la tabla y se consigan 10s datos, se desconecte el conjunto de datos de la
conexion y se rompa esta. El usuario no tiene porque saberlo y la aplicacion no
necesitara mantener una conexion abierta con la base de datos. El siguiente codi-
go muestra 10s dos pasos:
ADODataSetl.Connection : = nil;
ADOConnection1.Connected : = False;
Pooling de conexiones
Toda esta explicacion sobre el cierre de conexiones y su reapertura nos acerca
el tema delpooling de conexiones. Elpooling o reserva de conexiones, que no se
ha de confundir con el pooling de sesiones, permite que las conesiones a la base
de datos se reutilicen despues de que hayan finalizado. Esto se realiza de forma
automatica y, si nuestro proveedor OLE DB la soporta y esta activada, no es
necesario hacer nada para beneficiarnos del pooling de conesiones.
Existe una unica razon por la que querriamos utilizar esta tecnica para nues-
tras conesiones: el rendimiento. El problema de las conexiones a bases de datos
esta en que puede llevar cierto tiempo establecer una conexion. En una base de
datos de escritorio como Access, esto se reduce normalmente a un corto period0
de tiempo. En una base de datos clientelservidor como Oracle, que se utiliza en
una red, este tiempo podria medirse en segundos. Tiene sentido promover la
reutilizacion de este tipo de recurso tan costoso (en cuanto a rendimiento).
Si activamos la fusion de conexiones de ADO, 10s objetos Connection de ADO
se colocan en una cola cuando la aplicacion 10s "destruye". Los subsiguientes
intentos de crear una conesion ADO buscaran automaticamente en la cola de
conesion una conexion con la misma cadena de conesion. Si se encuentra una
conesion apropiada, se reutiliza. De no ser asi, se crea una nueva conexion. Las
propias conesiones permanecen en cola hasta que se reutilizan, se cierra la apli-
cacion o expiran. De manera predeterminada, las conesiones expiraran despues
de 60 segundos, pero a partir de MDAC 2.5 podemos configurar este tiempo
utilizando la clave de registro HKEY C L A S S E S R O O T \ C L S I D \ < Provi-
d e r C L S I D > \ S PT i r n e o u t . El procesi de poolingde conesion tiene lugar de
forma homogenea, sin la intervencion ni el conocimiento del desarrollador. Este
proceso es similar alpooling de bases de datos del BDE en Microsoft Transaction
Server (MTS) y COM+, con la importante escepcion de que ADO realiza su
propio pooling de conexiones sin la ayuda de MTS ni de COM+.
De manera predeterminada, elpooling de conexiones esta activado en 10s pro-
veedores MDAC OLE DB para bases de datos relacionales (como SQL Server y
Oracle), con la notable excepcion del proveedor Jet OLE DB. Si usamos ODBC,
deberiamos escoger entre el pooling de conexiones de ODBC y el de ADO, per0
no deberiamos utilizar ambos. A partir de MDAC 2.1, el pooling de conesiones
de ADO esta activado y el de ODBC esta desactivado.
Este guarda 10s datos y su delta en un archivo del disco duro. Podemos volver
a cargar dicho archivo utilizando el metodo LoadFromFile,que acepta un
solo parametro que indica el archivo que se ha de cargar. El formato del archivo
es Advanced Data Table Gram (ADTG), que es un formato propietario de
Microsoft. Sin embargo, tiene la ventaja de ser muy eficiente. Si lo preferimos,
podemos guardar el archivo como XML pasando un segundo parametro a
SaveToFile:
El modelo de maletin
Nuestro recientemente adquirido conocimiento sobre las actualizaciones por
lotes, conjuntos de registros desconectados y conjuntos de registros permanentes
nos permiten sacar partido del "modelo de maletin". La idea que se esconde detras
este modelo es que 10s usuarios quieren ser capaces de usar nuestra aplicacion
mientras e s t h de viaje, quieren llevarse la misma aplicacion que utilizan en 10s
escritorios de su oficina y utilizarla en sus portatiles mientras estan en las instala-
ciones de sus clientes. El problema de dichas circunstancias es que normalmente
cuando nuestros usuarios se encuentran en las instalaciones de sus clientes, no
estan conectados a su servidor de base de datos, porque el servidor de base de
datos esta en funcionamiento en la red interna de su propia oficina. En consecuen-
cia, no hay datos en el portatil y de todas formas no se pueden actualizar 10s
datos.
Es aqui donde entra en juego lo aprendido. Supongamos que la aplicacion ya
esta escrita. El usuario ha solicitado esta nueva ampliacion de maletin y tenemos
que adaptar la aplicacion existente. Es necesario afiadir una nueva opcion para
permitir que 10s usuarios que se "preparen" para la aplicacion de maletin ejecu-
tando sencillamente S a v e T o F i l e para cada tabla de la base de datos. El resul-
tad0 es una coleccion de archivos ADTG o XML en la que se refleja el contenido
de la base de datos. Estos archivos se copian entonces en el portatil en el que se ha
instalado previamente una copia de la aplicacion.
La aplicacion debera poder discernir si se esta ejecutando de forma local o
conectada a la red. Podemos determinarlo intentando conectar con la base de
datos y comprobando si no se nos permite hacerlo, detectando la presencia de un
archivo local de "maletin" o creando algun indicador de diseiio propio. Si la
aplicacion se esta ejecutando en mod0 de maletin, es necesario utilizar
L o a d F r o m F i l e para cada tabla en lugar de definir C o n n e c t e d como T r u e
para las conexiones ADOConnections y Act ive como True para 10s conjuntos
de datos ADO. Ademas, la aplicacion en mod0 de maletin necesita utilizar
SaveTo Fi le en lugar de UpdateBa t c h siempre que se guarden 10s datos. A1
volver a la oficina, el usuario necesita que haya un proceso de actualizacion que
cargue cada tabla del archivo local, se conecte el conjunto de datos a la base de
datos y aplique 10s cambios usando UpdateBatch.
Las grandes empresas suelen tener necesidades que son mucho mas amplias de
lo que pueden cubrir aplicaciones que usen bases de datos locales y servidores
SQL. En 10s ultimos aiios, Borland Software Corporation se ha enfrentado a las
necesidades de las grandes empresas e incluso carnbio temporalmente su nombre
por Inprise para subrayar su enfoque orientado a la empresa. Finalmente el nom-
bre volvio a cambiarse por Borland, pero el tener como objetivo el desarrollo de la
empresa sigue permaneciendo.
Delphi se dirige a muchas tecnologias diferentes: arquitecturas de tres capas
basadas en Windows NT y DCOM, aplicaciones TCPIIP y de sockets y, sobre
todo, servicios Web basados en XML y SOAP. Este capitulo se centra en las
arquitecturas multicapa orientadas a bases de datos, mientras que del resto de las
tecnologias se hablara mas adelante.
Antes de continuar, deberiamos resaltar dos elementos importantes. En primer
lugar, las herramientas para soportar este tip0 de desarrollo solo se encuentran
disponibles en la version Enterprise de Delphi; y, en segundo lugar, con Delphi 7
no hay que pagar derechos de desarrollo para aplicaciones DataSnap. Se adquiere
el entorno de desarrollo y despues se despliegan 10s programas en tantos servido-
res como se quiera, sin deber dinero a Borland. Se trata de un carnbio muy signi-
ficativo (el mas significativo en Delphi 7) de la politica de distribucion de DataSnap,
que solia requerir el pago de derechos por servidor (una cantidad inicialmente
muy elevada, que a lo largo del tiempo se fue reduciendo significativamente).
Esta nueva licencia de desarrollo aumentara con toda seguridad el atractivo de
DataSnap para 10s desarrolladores, que es una buena razon para comentar esta
herramienta con algo de detalle. Este capitulo trata 10s siguientes temas:
Arquitectura logica de tres capas.
Fundamento tecnico de DataSnap.
Los protocolos de conexion y 10s paquetes de datos.
Componentes de soporte de Delphi (de cliente y de sewidor).
El agente de conexion y otras caracteristicas extendidas.
La interfaz AppServer
Las dos partes de una aplicacion DataSnap se comunican mediante la interfaz
IAppServer.La definicion de esta interfaz se muestra en el listado 16.1. Es
extraiio que se necesite llamar directamente a 10s metodos de la interfaz
IAppServer,ya que Delphi incluye componentes que implementan esta interfaz
en las aplicaciones del lado del servidor y componentes que llaman a la interfaz en
las aplicaciones del cliente. Estos componentes simplifican el soporte de la interfaz
IAppServer y en ocasiones incluso la ocultan por completo. En la practica, el
servidor pondra a disposicion del cliente ob.jetos que implementen esta interfaz,
posiblemente junto con otras interfaces personalizadas.
type
IAppServer = interface (IDispatch)
[ ' /lAEPCC20- 7 A 2 4 - 1 lD2-9SBO-C69BEB+'B5B6D/ ']
function AS-Applyupdates (const ProviderName: WideString;
Delta: OleVariant;
MaxErrors: Integer; out Errorcount: Integer;
var Ownerdata: OleVarlant): OleVariant; safecall;
function AS-GetRecords (const ProviderName: WideString;
Count: Integer;
out RecsOut: Integer; Options: Integer; const
CommandText: WideString;
var Params: OleVariant; var OwnerData: OleVariant):
OleVarlant; safecall;
function AS-DataRequest(c0nst ProviderName: WideString;
Data: OleVariant): OleVariant; safecall;
function AS-GetProviderNames: OleVariant; safecall;
function AS-GetParams(const ProviderName: Widestring;
var Ownerdata: OleVariant): OleVariant; safecall;
function AS-RowRequest(const ProviderName: WideString; Row:
OleVariant;
RequestTpe: Integer; var OwnerData: OleVariant):
OleVariant; safecall;
procedure AS-Execute(const ProviderName: WideString;
const CommandText: WideString; var Params: OleVariant;
var OwnerData: OleVariant); safecall;
end;
- - -- -
Protocolo de conexion
DataSnap define unicamente la arquitectura de nivel superior y puede emplear
diferentes tecnologias para transferir datos desde la capa intermedia a1 entorno
del cliente. Soporta muchos protocolos distintos, entre 10s que destacan:
Distributed COM (DCOM) y Stateless COM (MTS o COM+): DCOM
esta disponible directamente en Windows NT/2000/XP y 98/Me, y no ne-
cesita aplicaciones en tiempo de e.jecucion adicionales en el servidor. DCOM
es basicamente una ampliacion de la tecnologia COM; que permite a las
aplicaciones cliente utilizar objetos de servidor que ya existen y ejecutar-
10s en un ordenador aparte. La infraestructura DCOM permite la utiliza-
cion de objetos COM sin estado (stateless), disponibles en las arquitecturas
de COM+ y de versiones anteriores de MTS (Microsoft Transaction Sewer).
COM+ y MTS ofrecen ciertas caracteristicas como seguridad, capacidad
de gestion de componentes y transacciones de bases de datos y estan dispo-
nibles en Windows NTl2000lXP y en Windows 98lMe. Debido a la com-
plejidad de configuracion de DCOM y sus problemas a la hora de atravesar
cortafuegos, incluso Microsoft trata de abandonar DCOM en favor de
soluciones basadas en SOAP.
Sockets TCPIIP: Estan disponibles en la mayoria de 10s sistemas. Usando
TCPIIP, podremos distribuir 10s clientes por la toda la Web, donde DCOM
no siempre es aplicable, y podemos ahorrarnos problemas de configura-
cion. Para utilizar 10s sockets, el ordenador de la capa intermedia debe
ejecutar la aplicacion scktsrvr . e x e proporcionada por Borland: un
sencillo programa ejecutable como aplicacion o como servicio. Este pro-
grama recibe las peticiones del cliente y las reenvia a1 modulo de datos
remoto (que se ejecuta en el mismo servidor) a traves de COM. Los sockets
no ofrecen ninguna proteccion frente a 10s posibles errores que puedan
surgir en el cliente, ya que el servidor no recibe informacion y podria no
liberar recursos~cuandoun cliente se desconecte de forma inesperada.
H T T P y SOAP: El uso de HTTP como protocolo de transporte de Internet
simplifica las conesiones a traves de 10s cortafuegos o 10s servidores inter-
medios o prosy (que no suelen gustar de 10s sockets TCPIIP personalizados).
.
Se requiere una aplicacion de servidor Web especifica, h t tpsrvr dl 1;
que acepte las solicitudes de 10s clientes y Cree 10s modulos de datos remo-
tos pertinentes mediante COM. Estas conexiones Web tambien pueden em-
plear la seguridad SSL. Ademas, las conexiones Web basadas en el
transporte HTTP pueden usar el soporte de interconesion de objetos de
DataSnap.
!nrlanci-g I ~ u l i i l eInstance
AI
m1
11
OEPT-NO~EMP-NO FIRST-NAME IFULL-WE IHIRE~-
-
) SUO , 2'R0beit Pidsan, Robnt 28/12
-E 2 b ~ ~ ~ ~ m w e c & o n ~ Yang, B~uce 28/12
130 5 Kim Lambert, Kim 6/2/1
Johnson, Lesb 5/4/1
- 622 Clia;DaaSdl 9 Phil Forest, Phil 17/11
- 130
000 i +
11 K. J.
12Te1ri
Wedm, K. J
Len. Terri
17tlt-
1/5/1
1 9 ~ "
~
623 DataSou~cel
149emt Hall. Stewart 4/6/1
- 15 Kalheme Y- Katherine
- 671 M Chr~s Papmkrpoulos. Chris
- 671 24 Pete F~her,Pete 1219/
- 120 28 Ann Bemet. Ann 1/2/
- 623 29 Roger De Swza. R o w 18/21
110 34 Janel B a l k . Jan&
Los programas de esta primera aplicacion de tres capas son obviamente muy
sencillos, aunque sirven de ejemplo sobre como crear un visualizador de conjun-
tos de datos capaz de repartir el trabajo entre dos ficheros ejecutables diferentes.
A estas alturas, nuestro cliente solo desempeiia funciones de visualizacion. Si 10s
datos se editan en el cliente, estos no se actualizaran en el servidor. Para llevar a
acabo esta operacion, sera precis0 aiiadir a1 programa algo de codigo adicional.
No obstante, antes de proceder con ello, aiiadiremos algunas caracteristicas mas
a1 servidor.
...............................,
,
L"*
,~!s.Jus.s-w ,- -. -. .--.
kodified ---
621 99912 Bruce 28/12/1988
- - -
Secuencia de actualizacion
Este programa cliente incluye tambidn un boton que sirve para aplicar las
actualizaciones a1 servidor y un dialog0 estandar de reconciliation. A continua-
cion. se ofrece un resumen de la secuencia completa de operaciones relacionadas
con una solicitud de actualizacion y 10s posibles eventos de error:
1. El programa cliente llama el metodo Applyupdates de un ClientDataSet.
2. El delta se envia a1 proveedor de la capa intermedia. El proveedor lanza el
evento OnUpdat e Dat a en el que se podran examinar las modificaciones
solicitadas antes de que dstas lleguen a1 senidor de la base de datos. En
este punto sera posible modificar el delta, que se transmite en un formato
compatible con 10s datos de un ClientDataSet.
3 . El proveedor (tecnicamente, una parte del proveedor llamada "resolver" o
resolutor) aplica cada una de las filas del delta a1 servidor de la base de
datos. Antes de aplicar cada actualizacion, el proveedor recibe un evento
Bef oreUpdateRecord. Si se ha activado el indicador ResolveTo-
DataSet, esta actualizacion acabara lanzando eventos locales del con-
junto de datos de la capa intermedia.
4. En caso de producirse un error de servidor, el proveedor lanzara el evento
OnUpda t e E r r o r (en la capa intermedia), dando a1 programa la oportu-
nidad de solucionar el error a dicho nivel.
5. Si el programa de la capa intermedia no soluciona el error, la peticion de
actualizacion correspondiente permanecera en el delta. El error se devuel-
ve a1 cliente en ese precis0 instante o una vez alcanzado un determinado
numero de errores, segun el valor del parametroMaxErrors de la llama-
da A p p l y U p d a t e s .
6. Por ultimo, el paquete delta que incluya las actualizaciones restantes se
reenvia a1 cliente, lanzando el evento 0 n R e c o n c i 1e E r r o r del
ClientDataSet para cada una de estas actualizaciones. En este controlador
de eventos, el programa cliente puede tratar de solucionar el problema
(posiblemente solicitando ayuda a1 usuario), modificando la actualizacion
en el delta y luego volviendola a enviar.
Refresco de datos
El metodo R e f r e s h del ClientDataSet permite obtener una version actuali-
zada de 10s datos que podrian haber sido modificados por otros usuarios. Sin
embargo, esta operacion solo podra realizarse si en la cache no figura ninguna
tarea de actualizacion pendiente, puesto que si se llama a1 metodo y no esta vacio
el registro de modificaciones, R e f r e s h lanzara una excepcion:
if cds .ChangeCount = 0 then
cds.Refresh;
RJresh Lop.
procedure TFOrrnl.Button2Click(Sender: T O b j e c t ) ;
var
i: Integer;
bm: TBookmarkStr;
begin
/ / refresca las filas visibles
cds.DisableControls;
/ / comienza con la fila actual
i := TMyGrid (DbGridl).ROW;
b m : = cds.Bookmark;
try
/ / vuelve a1 primer registro visible
while i > 1 do
begin
cds.Prior;
Dec (i);
end ;
/ / v u e l v e a 1 regis t r o a c t u a l
i : = TMyGrid (DbGridl).Row;
c d s - B o o k m a r k : = bm;
// s i g u e a d e l a n t e h a s t a q u e l a c u a d r i c u l a e s t a c o m p l e t a
w h i l e i < TMyGrid (DbGridl).Rowcount do
begin
cds .Next;
Inc (i);
end;
finally
// d e f i n e t o d o d e n u e v o y r e f r e s c a
c d s - B o o k m a r k : = bm;
cds.EnableControls;
end;
end;
Este enfoquc genera un trafico de red niuy denso, por lo que podria desearse
desencadenar actualizaciones unicamente cuando se produzcan modificaciones
rcales. Esta operacion se puede implcmentar aiiadiendo tecnologia de retrollamadas
a1 servidor, para que este pueda informar a todos 10s clientes conectados de que se
ha modificado un determinado registro. El cliente podra determinar si la modifi-
cation es de su interes y, en caso oportuno, lanzar la solicitud de rnodificacion.
Esta caracteristica permitira obtener mas registros que 10s requeridos por la
interfaz de usuario del cliente (la DBGrid). En otras palabras, se pueden conse-
guir directamente 10s registros sin esperar a que el usuario se desplace por toda la
cuadricula. Es aconsejable estudiar 10s detalles de estos ejemplos complejos des-
pues de la lectura del resto de esta seccion.
er SCUBA Company
IMl
FO Box Sn 91
I -
6582 N&'a SCURA L i i e d PO Box 6834 I
Figura 16.5. Formulario secundario del ejemplo ThinPlus, que muestra 10s datos de
una consulta por parametros.
Hay que tener en cuenta que tambien se puede llamar a metodos adicionales de
la interfaz COM a traves de DCOM, asi como utilizando una conexion de socket
o HTTP. Dado que el programa utiliza la convencion de llamada s a f e c a l l , la
exception que se genere en el servidor se reenvia y se muestra automaticamente
en el cliente. Asi, cuando un usuario marca la casilla de verificacion Connect, se
interrumpe el controlador de eventos utilizado para habilitar 10s conjuntos de
datos de cliente, de mod0 que un usuario con una clave de acceso incorrecta no
podra acceder a 10s datos.
Relaciones maestroldetalle
Si la aplicacion de la capa intermedia exporta diversos conjuntos de datos,
estos se pueden extraer por medio de diversos componentes ClientDataSet en la
parte del cliente y conectarlos localmente formando una estructura maestroldeta-
Ile. Esto ocasionara ciertos problemas para el conjunto de datos de detalle. a
menos que se estraigan todos 10s registros de forma local.
Esta solucion tambien dificulta la aplicacion de actualizaciones: normalmente,
un registro maestro no se puede cancelar hasta que se han eliminado todos 10s
rcgistros detallados relacionados, del mismo mod0 que tampoco es posible la
adicion de registros detallados hasta que el nuevo registro macstro esta ubicado
adecuadamente. (Distintos servidores se enfrentan a esta situacion de formas dis-
tintas, pero en la mayoria de 10s casos en 10s que se utilizan claves externas: este
es el procedimiento estandar.) Para solucionar este problema, se puede escribir
codigo complejo en el cliente para actualizar 10s registros de las dos tablas segun
las reglas especificas.
Un enfoque completamente diferente consiste en obtener un unico conjunto de
datos que ya incluya el detalle como un campo de conjunto de datos; un campo de
tipo T D a t a s e t F i e l d . Para ello es necesario preparar la relacion maestro/
detalle en la aplicacion de servidor:
o b j e c t Tablecustomer: TTable
DatabaseName = ' DBDEMOS'
TableName = ' cus torner. db'
end
o b j e c t Tableorders : TTable
DatabaseName = ' DBDEMOS'
MasterFields = ' C u s t N o '
Mastersource = DataSourceCust
TableName = ' ORDERS. DB'
end
o b j e c t DataSourceCust: TDataSource
DataSet = TableCustomer
end
o b j e c t Providercustomer: TDataSetProvider
DataSet = TableCustomer
end
Basicamente no hay que hacer nada mas. Para modificar la conexion fisica,
elegimos un nuevo componente de conexion DataSnap para el formulario princi-
pal y establecemos la propiedad Connection del agente para que utilice esa
conexion.
Pooling de objetos
Cuando varios clientes se conectan a1 servidor a1 mismo tiempo, existen dos
opciones: en primer lugar, se puede crear un objeto de modulo de datos remoto
para cada uno y permitir que cada solicitud sea procesada secuencialmente (el
comportamiento estandar de un servidor COM con el estilo ciMultiInstance). Por
otra parte, se puede dejar que el sistema Cree una instancia diferente de la aplica-
cion para cada cliente (ciSingleInstance). Este enfoque necesita mas recursos y
mas conexiones (y posiblemente licencias) del servidor SQL.
El soporte de DataSnap para el pooling de objetos ofrece un enfoque alternati-
vo. Todo lo que se necesita hacer para solicitar esta caracteristica es aiiadir una
llamada a Registerpooled en el metodo UpdateRegistry sobrescrito.
En combinacion con el soporte sin estado integrado en esta arquitectura, la capa-
cidad de pooling permite compartir ciertos objetos de la capa intermedia entre un
numero mucho mayor de clientes. En COM+ se incluye un mecanismo de pooling,
per0 DataSnap permite que este disponible tambien para conexiones basadas en
sockets y HTTP.
Los usuarios de 10s ordenadores cliente invertiran la mayor parte de su tiempo
leyendo y registrando actualizaciones, y en general no continuan solicitando datos
ni enviando actualizaciones. Cuando el cliente no llama a un metodo del objeto de
la capa intermedia, este modulo de datos remoto puede ser utilizado por otro
cliente. A1 carecer de estado, cada una de las solicitudes llega a la capa intermedia
como si fuese una operacion nueva, aun cuando un servidor este dedicado a un
cliente especifico.
La clase TDataLink
En gran parte de este capitulo trabajaremos con T D a t a L i n k y sus clases
derivadas, que estan definidas en la unidad DB. Este tipo cuenta con un grupo de
metodos virtuales protegidos, 10s cuales tienen una funcion muy similar a la de
10s eventos.
Se trata de metodos que "no hacen casi nada" y podemos sobrescribir en una
subclase especifica para interceptar operaciones del usuario y otros eventos de la
fuente de datos. A continuacion, aparece un listado extraida del codigo fuente de
esta clase:
type
TDataLink = class (TPersistent)
protected
procedure ActiveChanged; virtual;
procedure CheckBrowseMode; virtual;
procedure DataSetChanged; virtual;
procedure DataSetScrolled(Distance: Integer); virtual;
procedure FocusControl(Field: TFieldRef); virtual;
procedure Editingchanged; virtual;
procedure LayoutChanged; virtual;
procedure ~ e c o r d C h a n g e d ( F i e 1 d : T F i e l d ) ; virtual;
procedure UpdateData; virtual;
1R ~ N O 1
l ~ v e n l ~ o CUSINO INmTickelt l ~ m l _ ~ a i dI~y-~e(hod(Card-No 1-1
15 8 6 7 C52 50 DINERS 256335017856420371
B E40 00 DINERS 6146617034656232
6 C45.00 DINERS 481853612351817
3 E37.50 DINERS 2513715852358158
10 E50 00 DINERS 0521773736155304 - 1
Una TrackBar de lectura y escritura
El siguiente paso consiste en escribir un componente que permita a1 usuario
modificar una base de datos, no solo explorarla. La estructura global de este tip0
de componente no se diferencia demasiado de la version anterior, per0 existen
algunos elementos adicionales. En concreto, cuando el usuario comienza a
interactuar con el componente, el codigo debe poner a1 conjunto de datos en el
mod0 de edicion y, a continuacion, avisar a1 conjunto de datos que estos se han
modificado. En ese momento, el conjunto de datos utilizara un controlador de
eventos de F i e 1dDataL ink para pedir 10s valores actualizados.
Para mostrar como podemos crear un componente data-aware que modifique
10s datos, hemos ampliado el control TrackBar.No es el mas simple de 10s
ejemplos, pero muestra varias tecnicas importantes.
Esta es la definicion de la clase de componente (extraida de la unidad MdTrack
del paquete MdDataPack):
type
TMdDbTrack = class(TTrackBar)
private
FDataLink: TFieldDataLink;
function GetDataField: string;
procedure SetDataField (Value: string);
function GetDataSource: TDataSource;
procedure SetDataSource (Value: TDataSource);
function GetField: TField;
procedure CNHScroll(var Message: TWMHScroll); message
CN-HSCROLL;
procedure CNVScroll (var Message: TWMVScroll) ; message
CN-VSCROLL;
procedure CMExit(var Message: TCMExit); message CM-EXIT;
protected
/ / controladores de eventos de enlace de datos
procedure Datachange (Sender: TObject) ;
procedure UpdateData (Sender: TObject) ;
procedure Activechange (Sender: TObject) ;
public
constructor Create (AOwner: TComponent); override;
destructor Destroy; override;
property Field: TField read GetField;
published
property DataField: string read GetDataField write
SetDataField;
property Datasource: TDataSource read GetDataSource write
SetDataSource;
end;
Este codigo comprueba tres condiciones: el enlace de datos deberia estar acti-
vo, el enlace deberia hacer referencia a un campo real y el campo no deberia ser
de solo lectura. Cuando el usuario modifica el campo, el componente deberia
tener en cuenta que el nombre del campo podria no ser valido. Para comprobar
esta condicion, el componente usa un bloque try/finally:
procedure TMdDbTrack-SetDataField (Value: string);
begin
try
FDataLink.Fie1dName : = Value;
finally
Enabled : = FDataLink.Active and (FDataLink.Field <> nil)
and
not FDataLink.Field.Read0nly;
end ;
end ;
Cuando el conjunto de datos necesita datos nuevos, por ejemplo para realizar
una nueva operacion Post, lo unico que hace es solicitarlos a1 componente me-
diante el evento OnUpdateData de la clase TFieldDataLink:
p r o c e d u r e TMdDbTrack.UpdateData (Sender: TObject);
begin
if F D a t a L i n k - F i e l d i s TNumericField t h e n
FDataLink.Field.As1nteger : = Position;
end ;
Una vez mas, existe un programa de muestra para probar este componente,
podemos observar su salida en la figura 17.2. El programa DbTrack contiene una
casilla de verificacion que activa o desactiva la tabla, 10s componentes visuales y
un par de botones que podemos utilizar para separar el componente T r a c k B a r
vertical del campo a1 que esta relacionado. Se han colocado en el formulario para
comprobar la habilitacion e inhabilitacion de la barra de seguimiento.
Figura 17.2. Las barras de seguirniento del ejernplo DbTrack perrniten introducir
datos en una tabla de la base de datos. La casilla de verificacion y 10s botones
comprueban el estado de activacion de 10s cornponentes.
procedure TMdRecordLink.RecordChanged;
begin
inherited;
// l o p i n t a t o d o d e nuevo.. .
RView.Invalidate;
end ;
El codigo del enlace de registros es muy sencillo. La mayor parte de las difi-
cultades de la construccion de este ejemplo se deben a la utilizacion de la cuadri-
cula. Para evitar las propiedades innecesarias, hemos derivado la cuadricula del
visualizador de registros de la clase TCustomGrid.Esta clase incluye gran
parte del codigo para las cuadriculas, per0 la mayoria de sus propiedades, even-
tos y metodos estan protegidos. Esta es la razon por la que la especificacion de la
clase resulta bastante larga, ya que es necesario publicar muchas de las propieda-
des existentes. Este es un fragment0 de codigo (en el que se excluyen las propie-
dades de la clase basica):
type
TMdRecordView = class (TCustomGrid)
private
// s o p o r t a data-aware
FDataLink: TDataLink;
function GetDataSource: TDataSource;
procedure SetDataSource (Value: TDataSource) ;
protected
// rnetodos r e d e f i n i d o s TCustomGrid
procedure Drawcell (ACol, ARow: Longint ; ARect : TRect;
AState: TGridDrawState); override;
procedure ColWidthsChanged; override;
procedure RowHeightsChanged; override;
public
constructor Create (AOwner: TComponent) ; override;
destructor Destroy; override;
procedure SetBounds (ALeft, ATop, AWidth, AHeight:
Integer) ; override;
procedure Defineproperties (Filer: TFiler) ; override;
// p r o p i e d a d e s p a d r e p d b l i c a s ( o m i t i d a s . . . )
published
// p r o p i e d a d e s d a t a - a w a r e
property DataSource: TDataSource read GetDataSource write
SetDataSource;
/ / p r o p i e d a d e s p a d r e p u b l i c a d a s ( o m it i d a s . . . )
end ;
La cuadricula tiene dos columnas (una de ellas fija) y ninguna fila fija. La
columna fija se usa para modificar el tamaiio de cada fila de la cuadricula. Por
desgracia. el usuario no puede utilizar la fila fija para ajustar el tamaiio de las
columnas; ya que no es posible modificar el tamaiio de elementos fijos y la cuadri-
cula ya cuenta con una colurnna fija.
Esta modification del tamaiio tiene lugar cuando cambia el tamaiio del compo-
nente y cambia alguna de las columnas. Con este codigo, la propiedad
DefaultColWidth del componente se convierte en el ancho fijo de la primera
columna. Despues de haberlo preparado todo, el metodo clave del componente es
el metodo DrawCell sobrescrito, que se detalla en el listado 17.1. En este meto-
do, el control muestra la informacion sobre 10s campos y sus valores. Tiene que
representar tres cosas. Si el enlace de datos no esta conectado a una fuente de
datos, la cuadricula muestra un simbolo de "elemento vacio" ([]).Cuando se re-
presenta la primera colurnna, el visualizador de registros muestra la propiedad
DisplayName del campo, que es el mismo valor que el utilizado por la DBGrid
para el encabezamiento. A1 pintar la segunda colurnna, el componente accede a la
representacion textual del valor del campo, extraida de la propiedad
DisplayTex t (o con la propiedad Ass t r i n g para 10s campos de memo).
En la ultima parte del metodo, el componente tiene en cuenta 10s campos gra-
ficos y de memo. Si el campo es un TMemoField, la llamada a la funcion
DrawText no especifica el indicador dt SingleLine, sino que utiliza el
indicador dt WordBreak para partir las palabras cuando no hay espacio sufi-
ciente. ~ l a r o & i que para un campo grafico, el componente utiliza un enfoque
totalmente distinto, que consiste en asignar a la imagen de campo un mapa de bits
temporal y despues ampliarlo hasta que cubra la superficie de la celda.
Observe ademas que el componente establece como False la propiedad
DefaultDrawing, de tal forma que tambien es responsable de pintar el fondo
y el rectangulo de foco, igual que ocurre en el metodo Drawcell. El componen-
te tambien llama a la funcion In f 1 a t eRe ct de la API para dejar un pequeiio
espacio entre el borde de celda y el texto de salida. La salida real se obtiene a1
llamar a otra funcion de la API de Windows, DrawText, la cual centra el texto
verticalmente dentro de su celda.
Este codigo de representacion funciona tanto en tiempo de ejecucion, como
podemos comprobar en la figura 17.3, como en tiempo de diseiio. La salida puede
que no sea perfecta, per0 este componente puede ser util en muchos casos. Si
queremos mostrar 10s datos de un solo registro, en lugar de construir un formula-
rio personalizado con etiquetas y controles data-aware, podemos utilizar de for-
ma sencilla esta cuadricula de visualizacion de registros. No obstante, es importante
tener presente que el visualizador de registros es un componente de solo lectura.
Es posible ampliarlo para que adquiera capacidades de edicion (ya forman parte
de la clase TCustomGrid), pero, de todas formas, en lugar de aiiadir estc so-
porte, hemos decidido hacer que el componente sea mas completo afiadiendo so-
porte para mostrar campos BLOB.
Para mejorar la salida grafica, el control traza las lineas de 10s campos BLOB
el doble de altas que 10s campos de texto simple. Esta operacion se realiza una vez
que se activa el conjunto de datos conectado a1 control data-aware. El metodo
Activechanged del enlace de datos tambien se activa mediante 10s metodos
RowHeightsChanged conectados a la propiedad Def aultRowHeight de
la clase basica:
p r o c e d u r e TMdRecordLink.ActiveChanged;
var
I: Integer;
begin
// d e f i n e e l numero d e f i l a s
RView-RowCount : = DataSet.FieldCount;
/ / duplica la altura del memo y del grafico
f o r I : = 0 to DataSet.FieldCount - 1 d o
i f DataSet.Fields [I] is TBlobField then
RView.RowHeights [I] : = RView.DefaultRowHeight * 2;
// volver a pintarlo todo.. .
RView-Invalidate;
end ;
-I
Blue Angelhsh
11 81 1 a236220472
Habrtal 1s around bouldels. caves.
I
coral ledges and crevices m shallow
waters. Swims alone or in groups.
I
This is the la~gesld dl h e
uaassn It is l w n d in
W~asse Giant Maor1W~asse Che~l~nur
undulatus dense reef areas. 1eedh-g ,,:A
an a Wide valbty d
mobks, fishes, sea
Hab~tatis a w n d boulders,
caves. c n d ledges and
hwptlsh Bbe Angdlish Pmacanlhus nauarchus uewces n ~hanowwaters
Swrms alone or in pwps.
Mientras que para crear la salida tan solo hub0 que adaptar el codigo utilizado
cn el componente visualizador de registros, para establecer la altura de las celdas
de la cuadricula se planteo un problema de dificil solucion. iLas lineas del codigo
para esa operacion puede quc sean pocas, per0 cost6 horas llegar a esta solucion!
-
Canvas.Font : = Font;
PixelsPerRow : = Canvas.TextHeight ( ' W g ' ) + 3;
if dgRowLines i n Options then
Inc (PixelsPerRow, GridLineWidth) ;
Canvas.Font : = TitleFont;
PixelsTitle : = Canvas .TextHeight ( ' W g ' ) + 4;
if dgRowLines i n Options then
Inc (PixelsTitle, GridLineWidth);
// d e f i n e e l n u r n e r o d e f i l a s
RowCount : = 1 + (Height - PixelsTitle) div (PixelsPerRow *
FLinesPerRow) ;
// d e f i n e l a a l t u r a d e cada f i l a
DefaultRowHeight : = PixelsPerRow * FLinesPerRow;
RowHeights [0] : = PixelsTitle;
f o r I : = 1 to RowCount - 1 do
RowHeights [I] : = PixelsPerRow * FLinesPerRow;
-
end;
Lo mas dificil de conseguir en este metodo fue que las cuatro ultimas lineas
fueran correctas. Podemos definir la propiedad D e f a u l t R o w H e i g h t , per0 es
probable que en ese caso el titulo de fila sea demasiado alto. En un principio se
intento establecer la propiedad DefaultRowHeight y, despues, la altura de la
primera fila, per0 este enfoque complicaba el codigo usado para calcular el nume-
ro de filas visibles en la cuadricula (la propiedad de solo lectura V i s i b l e -
R o w C o u n t ) . Si especificamos el numero de filas, para que las filas no queden
escondidas debajo del extremo inferior de la cuadricula, la clase basica continua
sigue calculandolo. Este es el codigo usado para representar 10s datos, tomado del
componente R e c o r d v i e w y ligeramente adaptado para la cuadricula:
p r o c e d u r e TMdDbGrid.DrawColumnCel1 (const Rect: TRect; DataCol:
Integer;
Column: TColumn; State: TGridDrawState);
var
Bmp: TBitmap;
OutRect : TRect;
begin
i f FLinesPerRow = 1 t h e n
inherited DrawColurnnCell (Rect, DataCol, Column, State)
else
begin
// lirnpia l a zona
Canvas. FillRect (Rect);
// copia e l r e c t d n g u l o
OutRect := Rect;
// r e s t r i n g e l a s a l i d a
InflateRect (OutRect, - 2 , - 2 ) ;
// s a l i d a d e d a t o s d e l carnpo
i f Column.Field i s TGraphicField then
begin
Bmp : = TBitmap.Create;
try
Bmp.Assign (Column.Field) ;
Canvas .StretchDraw (OutRect, Bmp) ;
finally
Bmp.Free;
end ;
end
else i f Column.Field i s TMemoField then
begin
DrawText (Canvas-Handle, PChar
(Column.Field.AsString) ,
Length (Column.Fie1d.AsString), OutRect,
dt-WordBreak or dt-Noprefix)
end
else // dibuja una sola linea centrada verticalmente
DrawText (Canvas.Handle, PChar
(Column.Field.DisplayText),
Length (Column.Field. DisplayText) , OutRect,
dt-vcenter or dt-Singleline or dt-Noprefix);
end ;
end ;
En este codigo, se puede comprobar que si el usuario muestra una unica linea,
la cuadricula utiliza la tecnica estandar de representacion, sin salidas para cam-
pos graficos ni de memo. No obstante, en cuanto aumenta el numero de lineas se
podra comprobar como mejora la salida.
Para ver como funciona este codigo, hay que ejecutar el ejemplo GridDemo.
Este programa cuenta con dos botones que podemos utilizar para aumentar o
disminuir la altura de las filas de la cuadricula y otros dos mas para cambiar la
fuente. Se trata de una comprobacion importante, ya que la altura de cada celda
en pixeles es la altura de la fuente multiplicada por el numero de lineas.
// e n l a u n i d a d M d D s C u s t o m
type
EMdDataSetError = class (Exception);
TMdRecInfo = record
Bookmark: Longint;
BookmarkFlag: TBookmarkFlag;
end ;
PMdRecInfo = "TMdRecInfo;
// e n l a u n i d a d M d D s S t r e a r n
type
TMdDataFileHeader = record
VersionNumber: Integer;
Recordsize: Integer;
Recordcount: Integer;
end :
// i n i c i a l a s d e f i n i c i o n e s d e campos
InternalInitFieldDefs:
// s i n o h a y o b j e t o s d e campo p e r m a n e n t e s , c r e a 1 0 s c a m p o s d e
/ / forma d i n d m i c a
if DefaultFields then
CreateFields;
// c o n e c t a 1 0 s o b j e t o s T F i e l d c o n 1 0 s c a m p o s r e a l e s
BindFields (True);
InternalAfterOpen; / / m e t o d o p e r s o n a l i z a d o p a r a subclases
// d e f i n e 1 0 s c r a c k s y l a p o s i c i o n y tamado d e l r e g i s t r o
BofCrack : = - 1 ;
EofCrack : = InternalRecordCount;
FCurrentRecord : = BofCrack;
FRecordBufferSize : = FRecordSize + sizeof (TMdRecInfo);
Bookmarksize : = sizeof (Integer);
// t o d o O K : a h o r a l a t a b l a e s t d a b i e r t a
FIsTableOpen : = True;
end;
Podemos ver que el metodo define gran parte de 10s campos locales de la clase,
asi como el campo Boo kmar kSi ze de la clase base TDa taSet . En este meto-
do, se ha llamado a dos metodos personalizados que se han introducido en la
jerarquia del conjunto de datos personalizado: I n t e r n a 1 P r e o p e n e
InternalAfterOpen.
El primero, Internal PreOpen, se utiliza para operaciones que son nece-
sarias a1 principio, como la comprobacion de si puede abrirse el conjunto de datos
y la lectura de la informacion del cabecera del archivo. El codigo comprueba si un
numero de version interno se corresponde con el valor que se guard6 cuando se
creo la tabla por primera vez. A1 crear una excepcion en este metodo, podemos
detener en ultimo termino la operacion de apertura.
A continuacion, aparece el codigo de 10s dos metodos del conjunto de datos
derivado basado en streams:
cons t
HeaderVersion = 10;
procedure TMdDataSetStream-InternalPreOpen;
begin
// e l tamafio d e l a c a b e c e r a
FDataFileHeaderSize : = sizeof (TMdDataFileHeader);
// v e r i f i c a s i e x i s t e e l a r c h i v o
i f n o t FileExists (FTableName) then
r a i s e EMdDataSetError .Create ( ' O p e n : T a b l e f i l e not
found' ) ;
// c r e a u n s t r e a m p a r a e l a r c h i v o
F S t r e a m : = T F i l e S t r e a m - C r e a t e (FTableName, fmOpenReadWrite);
// i n i c i a d a t o s l o c a l e s (cargando el t i t u l o )
F S t r e a m - R e a d B u f f e r (FDataFileHeader, FDataFileHeaderSize);
i f FDataFileHeader-VersionNumber <> HeaderVersion then
r a i s e EMdDataSetError. Create ( ' I l l e g a l P i l e V e r s i o n ' ) ;
// v a m o s a l e e r e s t o , v e r i f i c a r d e n u e v o m d s a d e l a n t e
FRecordCount : = FDataFi1eHeader.RecordCount;
end;
procedure TMdDataSetStream.InternalAfter0pen;
begin
// v e r i f i c a e l t a m a f i o d e l r e g i s t r o
i f FDataFi1eHeader.RecordSize <> FRecordSize then
r a i s e EMdDataSetError-Create ( ' F i l e r e c o r d s i z e
mismatch' ) ;
// v e r i f i c a e l n u m e r o d e r e g i s t r o s en o p o s i c i o n a 1 t a m a f i o d e l
// a r c h i v o
i f (FDataFileHeaderSize + FRecordCount * FRecordSize) <>
FStream-Size then
r a i s e EMdDataSetError .Create ( ' I n t e r n a l o p e n : I n v a l i d
Record S i z e ' ) ;
end ;
[Fields]
Number = 6
[Fieldl]
Type = f t S t r i n g
Name = Name
S i z e = 30
[Field2]
Type = f t I n t e g e r
Name = L e v e l
[Field31
Type = f t D a t e
Name = B i r t h D a t e
[Field41
Type = f t c u r r e n c y
Name = S t i p e n d
[Fields]
Type = f t S t r i n g
Name = Email
S i z e = 50
[Field61
Type = f t B o o l e a n
Name = E d i t o r
Este archivo, o uno similar, debe tener el mismo nombre que el archivo de
tabla y debe ubicarse en el mismo directorio.
El metodo InternalInit FieldDef s (que se muestra en el listado 17.4)
lo leera utilizando 10s valores que encuentra para establecer las definiciones de
campo y determinar el tamaiio de cada registro.
El metodo tarnbien inicia un objeto TLis t interno que almacena el desplaza-
miento de cada campo dentro del registro. Se utiliza esta TLis t para acceder a
10s datos de 10s campos en el buffer de registro, como se puede comprobar en el
fragment0 de codigo.
Listado 17.4. El metodo InternallnitFieldDefs del conjunto de datos basado en
streams.
procedure TMdDataSetStream.InternalInitFie1dDefs;
var
IniFileName, FieldName: string;
IniFile: TIniFile;
nFields, I, TmpFieldOff set, nSize: Integer;
FieldType: TFieldType;
begin
FFieldOffset : = TList.Create;
FieldDefs-Clear;
TmpFieldOffset : = 0;
IniFilename : = ChangeFileExt(FTableName, '.init);
Inifile : = TIniFile.Create (IniFilename);
// protege el archivo INI
try
nFields : = IniFile.ReadInteger ( ' Fields' , 'Number', 0) ;
if nFields = 0 then
raise EDataSetOneError-Create ( ' InitFieldsDefs: 0
fields?' ) ;
for I : = 1 to nFields do
begin
// crea el campo
FieldType : = TFieldType (GetEnumValue (TypeInfo
(TFieldType),
IniFile. Readstring ( 'Field' + IntToStr (I), ' Type',
' I ) ) ) ;
Para cerrar la tabla, solo hay que desconectar 10s campos utilizando llamadas
estandar. Cada clase debe encargarse de 10s datos que asigno y actualizar la
cabecera del archivo, la primera vez que se aiiaden 10s registros y cada vez que se
modifique el numero de registros:
procedure TMDCustomDataSet.InternalClose;
begin
// d e s c o n e c t a r o b j e t o s d e campo
BindFields (False);
// d e s t r u y e e l o b j e t o d e campo ( s i n o e s p e r m n e n t e )
i f DefaultFields then
DestroyFields;
// c i e r r a e l a r c h i v o
FIsTableOpen : = False;
end ;
procedure TMdDataSetStream.InternalClose;
begin
// s i e s n e c e s a r i o , g u a r d a l a c a b e c e r a a c t u a l i z a d a
if (FDataFi1eHeader.RecordCount <> FRecordCount) or
(FDataFi1eHeader.RecordSize = 0) then
begin
FDataFi1eHeader.RecordSize : = FRecordSize;
FDataFi1eHeader.RecordCount : = FRecordCount;
i f Assigned (FStrearn) then
begin
FStream. Seek (0, soFromBeginning) ;
FStrearn.WriteBuffer (FDataFileHeader,
FDataFileHeaderSize);
end ;
end ;
// l i b e r a 1 0 s d e s p l a z a m i e n t o s d e campo d e l a l i s t a i n t e r n a y
el stream
FField0ffset.Free;
FStream. Free;
i n h e r i t e d Internalclose;
end ;
Estos son 10s metodos de apertura y cierre que debemos implementar en cual-
quier conjunto de datos personalizados. No obstante, en la mayoria de 10s casos,
tambien tendremos que aiiadir un metodo para crear la tabla. En este ejemplo, el
metodo CreateTable crea un archivo vacio e inserta informacion en la cabe-
cera: un numero fijo de version, un tamaiio de registro ficticio (no se conoce el
tamaiio real hasta que se inicien 10s campos) y el recuento de registros (que a1
empezar sera cero):
procedure TMdDataSetStream-CreateTable;
begin
CheckInactive;
InternalInitFieldDefs;
// c r e a e l a r c h i v o n u e v o
if FileExists (FTableName) then
raise EMdDataSetError.Create ( ' P i l e ' + FTableName + ' .
already e x i s t s ' );
FStream : = TFileStream.Create (FTableName, fmCreate or
fmShareExclusive);
try
// guarda l a c a b e c e r a
FDataFi1eHeader.VersionNumber : = Headerversion;
FDataFileHeader .Recordsize : = 0; / / s e u t i l i z a mds t a r d e
FDataFileHeader .Recordcount : = 0; // v a c i o
FStream-WriteBuffer (FDataFileHeader, FDataFileHeaderSize);
finally
// c i e r r a e l a r c h i v o
FStream. Free;
end ;
end :
FRecordSize
Bookmark BookmarkFlag
Figura 17.5. La estructura de cada buffer del conjunto de datos personalizado, junto
con 10s diversos campos locales que hacen referencia a sus partes.
Para acceder a 10s marcadores y 10s indicadores, se puede utilizar como des-
plazamiento el tamaiio de 10s datos reales, convirtiendo el valor a1 tip0 de puntero
P M d R e c Inf o. Despues habra que acceder a1 campo apropiado de la estructura
T M d R e c Inf o mediante el puntero.
Los dos metodos que se utilizan para establecer y obtener 10s indicadores de
marcador muestran esta tecnica:
procedure TMDCustomDataSet.SetBookmarkF1ag (Buffer: PChar;
Value: TBookmarkFlag);
begin
PMdRecInfo(Buffer + FRecordSize).BookmarkFlag : = Value;
end;
procedure TMDCustomDataSet.Interna1Last;
begin
EofCrack : = InternalRecordCount;
FCurrentRecord : = EofCrack;
end :
p r o c e d u r e TMDCustomDataSet.SetRecNo(Value: Integer);
begin
CheckBrowseMode;
i f (Value > 1 ) and (Value <= FRecordCount) t h e n
begin
FCurrentRecord : = Value - 1;
Resync ( [ I ) ;
end ;
end;
La razon para reservar memoria de este mod0 es que el conjunto de datos suele
aiiadir mas informacion a1 buffer del registro, por lo tanto el sistema no puede
saber cuanta memoria debe asignar. Hay que fijarse en que, en el metodo
A1 1ocRecordBuf f er, el componente reserva la memoria para el buffer de
registro, en el que se incluyen tanto 10s datos de la base de datos como la informa-
cion de registro. En el metodo Internalopen se escribio:
FRecordBufferSize : = InternalRecordSize + sizeof (TMdRecInfo);
El componente tambien necesita implementar una funcion para reiniciar el
buffer, InternalI nit Recor d,generalmente rellenandolo con ceros numeri-
cos o espacios.
Por sorprendente que parezca, tambien se debe implementar un metodo que
devuelva el tamaiio de cada registro, per0 solo del fragment0 de datos, no del
buffer de registro completo. Este metodo es necesario para implementar la propie-
dad de solo lectura Re cordSi ze,que solo se utiliza en un par de casos particu-
lares en todo el codigo fuente de la VCL. En el conjunto de datos personalizado
generico, el metodo G e t R e c o r d S i z e devuelve el valor del campo
FRecordSize.
Hemos llegado a1 nucleo de un componente de conjunto de datos personaliza-
do. Los metodos de este grupo son GetRecord,que lee 10s datos desde el archi-
vo, InternalPost e InternalAddRecord,que actualizany aiiadennuevos
datos a1 archivo e I nt ernal De l e te, que elimina 10s datos y no esta
implementado en este conjunto de datos de muestra.
El metodo mas complejo de este grupo probablemente sea GetRecord,que
sirve para fines muy diversos. De hecho, el sistema utiliza este metodo para
recuperar 10s datos del registro actual, rellenar un buffer pasado como parametro
y conseguir 10s datos del registro anterior o del siguiente. El parametro GetMode
determina su accion:
tme
TGetMode = (gmcurrent, gml\lext, gmPrior);
esta utilizando una representacion de cadena para todos 10s campos. Si utilizamos
esta prueba, 10s valores enteros que Sean cero y las cadenas vacias apareceran
como valores nulos (10s controles data-aware estaran vacios). El problema es que
10s valores booleanos falsos no aparecen o, todavia peor, 10s valores de coma
flotante sin decimales y con pocos digitos no se mostraran, ya que la parte
exponencial de su representacion sera cero.
No obstante, para conseguir que este ejemplo funcione, es necesario conside-
rar como vacios 10s campos de fecha y hora con un cero inicial. Sin este codigo,
Delphi intenta convertir la fecha cero interna no valida (internamente, 10s campos
de datos no utilizan un tipo de datos T D a t e T i m e , sino una representacion dife-
rente) creando una excepcion. El codigo funcionaba con las versiones anteriores
de Delphi.
--. - - -
[- ADYERTENCIA: Al tratar & salucionar este problems, tambib hemos
descubierto b e si se llama a I s ~ u l lpara un campo, esta pctici6n se
resuehe I W d o a Get FieldData sin que se pase ningun buffer a relle-
nar, sino GCmprobando el resultado de la llama& a esta funcion. Este b el
motivo de 1a comprobacion if A s signed ( Buffer ) dentr.o del c6digo.
ad 1 A-
Solo existe otro metodo que se utiliza para guardar 10s datos del registro ac-
tual en el buffer del registro, incluyendo la informacion sobre 10s marcadores.
Los datos centrales se reducen a la posicion del registro actual, que se correspon-
de con el indice de la lista (y tambien con el marcador):
procedure TMdListDataSet.Interna1LoadCurrentRecord (Buffer:
PChar) ;
begin
PInteger (Buffer)* := fCurrentRecord;
with PMdRecInfo (Buffer + FRecordSize) " do
begin
BookmarkFlag : = bfcurrent;
Bookmark : = fCurrentRecord;
end;
end ;
Para cada carpeta se llama a este constructor durante la apertura del conjunto
de datos:
procedure TMdDirDataset.InternalAfter0pen;
var
Attr: Integer;
FileInfo: TSearchRec;
FileData: TFileData;
begin
// define todos 10s archivos
Attr : = faAnyFile;
FList .Clear;
if SysUtils.FindFirst(fDirectory, Attr, FileInfo) = 0 then
repeat
FileData : = TFileData. Create (FileInfo);
FList .Add (FileData);
until SysUtils. FindNext (FileInfo) <> 0;
SysUtils.FindClose(FileInfo);
end;
El siguiente paso es definir 10s campos del conjunto de datos que; en este caso,
son fijos y dependen de 10s datos disponibles del directorio:
procedure TMdDirDataset.InternalInitFie1dDefs;
begin
i f fDirectory = " then
raise EMdDataSetError.Create ('Missing directory');
// definiciones de campos
FieldDefs.Clear;
FieldDefs .Add ( 'FileNamel, ftstring, 40, True) ;
FieldDefs .Add ( ' Timestamp', ftDateTime) ;
FieldDefs .Add ( ' Size1, ftInteger) ;
FieldDefs .Add ( 'Attributes', ftstring, 3) ;
FieldDefs.Add ('Folder', ftBoolean);
end;
Por ultimo, el componente tiene que mover 10s datos desde el objeto de la lista
a1 que hace referencia el buffer del registro actual (el valor A c t i v e B u f f e r )
hasta cada uno de 10s campos del conjunto de datos, tal como lo solicita el metodo
G e t F i e l d D a t a . Esta funcion utiliza Move o S t r C o p y , segun el tipo de da-
tos, y realiza algunas conversiones para 10s codigos de atributos (H para 10s
ocultos, R para 10s de solo lectura y s para 10s de sistema) que sc extraen de 10s
indicadores correspondientes y que tambien se utilizan para determinar si un ar-
chive es realmente una carpeta. A continuacion, se muestra el codigo:
function TMdDirDataset.GetFie1dData (Field: TField; Buffer:
Pointer) : Boolean;
var
FileData: TFileData;
Booll: WordBool;
strAttr: string;
t : TDateTimeRec;
begin
FileData : = List [PInteger (ActiveBuffer)" 1 as TFileData;
case Field. Index of
0 : // nombre de archivo
StrCopy (Buffer, pchar(FileData.ShortFi1eName));
1: // s e l l o d e ultima m o d i f i c a c i d n
begin
t := DateTimeToNative (ftdatetime, FileData.Time) ;
Move (t, Buffer", sizeof (TDateTime)) ;
end ;
2: / / tarnafio
Move (FileData.Size, Buffer", sizeof (Integer));
3: / / a t t r i b u t o s
begin
strAttr : = ' 1 .
end;
Figura 17.7. La salida del ejernplo DirDemo, que usa un conjunto de datos algo
inusual para rnostrar datos de directorio.
.- -- - -
ADVERTENCIA: Si la v e m h & Win&m que ~sewtiILa h n e proble-
mas con los cdroles de la shell & e-b disponibles a Dew,sq p d e
usar k versih DirDemoNoShell del ejempio, qae asa 1- v i e s d o l e s
de arcihivos de Delphi ~ornpatiblescon Windows 3.1.
type
TMdObjDataSet = class (TMdListDataSet)
private
PropList: PPropList;
nProps : Integer;
FObjClass: TPersistentClass;
ObjClone: TPersistent;
FChangeToClone: Boolean;
procedure SetObjClass (const Value: TPersistentClass);
function Getobjects (I: Integer) : TPersistent;
procedure SetChangeToClone (const Value: Boolean);
protected
procedure InternalInitFieldDefs; override;
procedure Internalclose; override;
procedure InternalInsert; override;
procedure InternalPost; override;
procedure Internalcancel; override;
procedure InternalEdit; override;
procedure SetFieldData (Field: TField; Buffer: Pointer) ;
override;
function GetCanModify: Boolean; override;
procedure Internalpreopen; override;
pub1 i c
function GetFieldData ( Field: TField; Buffer: Pointer) :
Boolean; override;
property Objects [I: Integer] : TPersistent read
GetObj ects;
function Add: TPersistent;
published
property ObjClass : TPersistentClass read FObjClass write
SetObjClass;
property ChangeToClone: Boolean read FChangeToClone
write SetChangeToClone default False;
end :
for i : = 0 to nProps - 1 do
case PropList [i] .PropTypeA .Kind of
tkInteger, tkEnumeration, tkset:
FieldDefs.Add (PropList [i].Name, ftInteger, 0);
tkChar: FieldDefs.Add (PropList [i].Name, ftFixedChar, 0) ;
tkFloat: FieldDefs.Add (PropList [i].Name, ftFloat, 0);
tkstring, tkLString:
FieldDefs.Add (PropList [i].Name, ftstring, 50);
// TODO: a r r e g l a r t a m a d o
tkwstring: FieldDefs.Add (PropList [i].Name,
ftWideString, 50) ;
/ / TODO: a r r e g l a r t a m a f i o
end ;
end ;
procedure TObjDataSet.Interna1Post;
begin
i f FChangeToClone and Assigned (Obj Clone) then
DoClone (ObjClone, TDbPers(fList [fCurrentRecord]));
end ;
procedure TObjDataSet.InternalCance1;
begin
i f n o t FChangeToClone and Assigned (ObjClone) then
DoClone (ObjClone, TPersistent (List [fCurrentRecord]) ) ;
end :
21 3242.43 12
33 6716,54
28 -
3722,38
24 4747.94 23
John 62 597,24 14
Las aplicaciones de bases de datos permiten ver y editar datos, per0 normal-
mente 10s datos deberian imprimirse fisicamente en papel. Tecnicamente, Delphi
soporta la impresion de muchas maneras distintas, desde la salida directa de texto
a1 uso de la impresora C a n v a s , desde sofisticados informes de bases de datos a
la generacion de documentos en varios formatos (como Microsoft Word u
Openoffice de Sun). En este capitulo vamos a centrarnos en 10s informes y en
particular en el uso del motor de informes Rave, una herramienta de terceras
partes incluida en Delphi 7. Si se tiene interes en otras tecnicas de Delphi para
controlar la impresora, puede estudiarse el material relacionado con este tema en
el sitio Web del autor.
Las herramientas de generacion de informes son importantes porque pueden
llevar a cab0 procesos complejos por si mismas. El subsistema de informes puede
convertirse en una aplicacion independiente. Aunque este capitulo se centra en el
mod0 de producir un informe a partir de un conjunto de datos desde 10s progra-
mas Delphi, siempre deberia tenerse en cuenta la naturaleza autonoma de 10s
informes a1 evaluar una herramienta de este tipo.
Hay que crear 10s informes con cuidado, ya que representan una interfaz de
usuario para la aplicacion que transciende, y que a veces es mas importante que el
propio software. Es normal que haya mas gente que se fije en 10s informes impre-
sos que usuarios que generen 10s informes mediante 10s programas. Es por esto
que resulta importante disponer de informes de gran calidad y de una arquitectura
flexible que permita que 10s usuarios puedan personalizarlos.
Presentacion de Rave
Los informes son uno de 10s principales medios de adquisicion de informacion
a partir de 10s datos que se manejan en una aplicacion. Para resolver 10s proble-
mas vinculados con la presentacion de un informe visual de datos que resulte
claro y lleno de significado, las aplicaciones tradicionales de generacion de infor-
mes visuales han proporcionado herramientas de disposicion en bandas con la
intencion de proporcionar listas de datos con el aspect0 de tablas. Sin embargo,
hoy en dia esisten informes con necesidades mas completas que no se manejan
con facilidad mediante estas herramientas.
Rave Reports es un entorno de diseiio de informes visuales que ofrece muchas
prestaciones unicas que ayudan a simplificar, acelerar y volver mas eficaz el
proceso de generacion de informes. Rave puede manejar una gran variedad de
formatos de informes e incluye tecnologias avanzadas como las copias reflejo
para fomentar la reutilizacion del contenido de 10s informes con el fin de conse-
guir un mantenimiento mas sencillo y cambios mas rapidos.
Este capitulo sirve como breve presentacion de las caracteristicas de Rave. Se
puede encontrar informacion adicional sobre Rave en 10s archivos de ayuda, en la
documentacion PDF que se encuentra en el CD de Delphi, en varios proyectos de
muestra; y en el sitio Web del fabricante del software, www.nevrona.com.
--.-
NOTA: Una caracteristica clave de Rave (y una de las razones por las que
Borland ha escogido este product0 sobre otras soluciones) es que es una
solucion completamentemultiplatafonna que puede usarse tanto en Windows
como en Linux. No solo 10s componentes de Rave se integran tanto con la
Rave: el entorno visual de creacion de informes
Para arrancar el entorno de diseiio de informes visuales Rave, hay que hacer
doble clic sobre un componente TRvPro j e c t que haya sobre un formulario o
seleccionar Tools>Rave Designer en el IDE de Delphi. Cuando se active este
entorno, se vera una ventana como la que muestra la figura 18.1. Como se puede
comprobar, el Rave Designer incluye varias secciones: el Page Designer, el
Event Editor, el panel Property, el panel Project Tree, barras de herramientas,
la Toolbar Palette y la barra de estado.
NOTA: S i e m p ~ - q w , squiera
e yer el resultado dcl trabajo, habra que pul-
sar la tech F9 c% e l k w e Designer para tener una vista previa del infor-
-
Hay que tener presente que Rave permite que 10s usuarios finales puedan crear
o modificar sus propios informes. Se puede configurar Rave Designer a nivel
Beginner, Intermediate o Advanced mediante el cuadro de dialogo Edit>
Preferences (en la seccion Environment), para que 10s usuarios finales traba-
jen a1 nivel con el que se sientan comodos y no dispongan de mas potencia de la
que se les quiera ofrecer.
Tambien se pueden bloquear algunas caracteristicas del informe para que no
Sean modificables.
El Page Designer y el Event Editor
La parte central de la ventana del Rave Designer contiene el Page Designer
(donde se estructura el informe) y el Event Editor (donde se pueden crear guio-
nes para particularizar el informe en tiempo de ejecucion).
El Page Designer es el aspecto mas sobresaliente de Rave. Esta pagina es la
base de un informe, donde se realizan todas las acciones de diseiio. La pagina
muestra una cuadricula, aunque puede modificarse el aspecto de la pagina me-
diante 10s parametros de preferencias. Los nombres de las paginas en proceso de
diseiio en cada momento aparecen en las pestaiias que se encuentran en la parte
superior del Page Designer (Page1 en la figura).
El Event Editor permite definir codigo interpretado personalizado para 10s
componentes del informe. Cada componente tiene distintos tipos de eventos que
pueden usarse para realizar calculos, manipular cadenas de texto o aplicar una
logica particular a1 informe. El Event Editor es una caracteristica avanzada de
Rave, por ello hablaremos brevemente sobre ella a1 final de este capitulo.
El panel Property
El panel Property que se encuentra a la izquierda en el Rave Designer
ayuda a personalizar el aspecto o comportamiento de 10s componentes. Este panel
tiene una funcion parecida a la del Object Inspector de Delphi: cuando se selec-
ciona un componente en la pagina, el panel Property refleja la seleccion realiza-
da mostrando las distintas propiedades asociadas con ese componente. Si no hay
seleccionado ningun componente, el panel esta en blanco.
A1 igual que en el IDE de Delphi, se puede modificar el valor de una propiedad
manipulando el contenido del cuadro de edicion, seleccionando una opcion de una
lista desplegable o haciendo que aparezca un cuadro de dialogo de edicion. Se
puede hacer doble clic sobre cualquier propiedad que tenga una lista de opciones
(en lugar de hacer clic sobre el boton Flecha abajo y seleccionar a continuacion
la opcion) para pasar a1 siguiente elemento de la lista.
La barra de estado
En la parte inferior del Rave Designer se encuentra la barra de estado. Esta
barra de estado proporciona informacion sobre el estado de la conexion de la vista
de datos directa y la posicion y tamafio del raton. El color de 10s indicadores de la
conexion de datos permiten conocer el estado del sistema de datos de Rave
(DirectDataView): gris y verde indican una conexion inactiva o activa, respecti-
vamente; amarillo y rojo indican situaciones especificas del acceso a datos (res-
pectivamente, espera de una respuesta o fuera de plazo).
Los valores X e Y son las coordenadas del cursor del raton en las unidades de
pagina. Cuando se deja caer un componente sobre la pagina, si no se suelta el
boton del raton, se mostrara el tamafio del componente mediante 10s valores dX y
dY (con d de delta, incremento).
I And more text And more text. And more text. And more text. And more
text And more text And more text. And more text.
And more text And more text. And more text. And more text. And more
I
text And more text. And more text. And more text. And more text. And
more text And more text. And more text. And more text. And more text
And more text And more text.
I
I
-- -
.----
And more text. And more text. And more text. And more l e x t And more
text. And more text And more text. And more text. And more text And
- - .- -
.- - .
"I
- -- -- A\
Figura 18.2. La ventana de vista previa del inforrne de Rave.
ha comentado, el
todos 10s cornponentes disponibles en ~ a v Reports
e
camp-
-
pue.de usarst en h a
nnliracilin VCl . n rl .X El mntnr R n v e au rmnletmmente mi~ltinlatafnrma
Para controlar 10s parametros mas importantes del informe y la vista previa,
puede conectarse un componente RvNDRWriter o RvSystem con la propiedad
E n g i n e del componente RvProject:
Componente RvNDRWriter: Genera un archivo con formato NDR cuan-
do se ejecuta el informe. Los archivos con formato NDR son archivos con
un formato binario propietario y almacenan toda la informacion que nece-
sitan 10s componentes de representacion para reproducir el informe en una
gran variedad de formatos.
componente RvSystem: Combina el componente RvNDRWr iter con
una interfaz de usuario estandar de representacion, vista previa e impre-
sion. Esisten muchas propiedades para personalizar la interfaz de usuario,
y se pueden definir eventos sobrescritos para sustituir 10s dialogos estandar
con versiones particularizadas.
Formatos de representacion
El motor Rave produce un archivo o flujo NDR, generado por el
RvNDRWr iter.El motor de representacion de Rave puede convertir esta repre-
sentacion interna en una amplia gama de formatos. Para permitir a un usuario
escoger entre uno de 10s formatos para el archivo final, hay que colocar 10s com-
ponentes de representacion deseados dentro de un formulario del programa Delphi.
Cuando se ejecuta el metodo Execute del componente RvProject (como en
el ejemplo Raveprint), se puede escoger uno de 10s formatos de archivo en el
cuadro de dialog0 que muestra Rave y que se puede ver en la figura 18.3.
Conexiones de datos
Los componentes de conexion de datos proporcionan un enlace entre 10s datos
contenidos en una aplicacion Delphi y las DirectDataView disponibles en el Rave
Designer. Fijese en que el valor definido en la propiedad N a m e de cada compo-
nente de conexion de datos se utiliza para proporcionar el enlace con el informe de
Rave. Es por este motivo por el que hay que tener cuidado en evitar modificar 10s
nombres de componentes despues de que se creen las DirectDataViews en Rave.
Los componentes de conexion de datos son 10s siguientes:
RvCustomConnection: Proporciona datos a 10s informes de Rave me-
diante eventos programados y puede usarse para enviar datos que no for-
men parte de una base de datos a un informe visual.
RvDataSetConnection: Conecta cualquier componente descendiente de la
clase T D a t a S e t con una DirectDataView de Rave. Mediante la propie-
dad F i e l d A l i a s L i s t tambien se pueden modificar 10s nombres de 10s
campos del conjunto de datos, sustituyendolos por nombres mas expresi-
vos para desarrolladores o usuarios finales que vayan a crear el informe.
Si se necesita ordenacion o filtrado para busquedas o relaciones maestro1
detalle en 10s informes de Rave, se pueden controlar 10s eventos
OnSetSort y OnSetFilter.
Componentes basicos
La barra de herramientas Standard tiene siete componentes: Text, Memo,
Section, Bitmap, Metafile, FontMaster y PageNumInit. Muchos de 10s compo-
nentes estandar se suelen utilizar cuando se diseiian informes.
Componentes Text y Memo
El componente Text es util para mostrar una unica linea de texto en el informe.
Actua como una etiqueta que puede contener texto sencillo (datos no). Cuando se
coloca en el informe, el cuadro de texto queda rodcado por un marco que indica
sus bordes. Los componentes Memo son parecidos a 10s componentes Text, per0
pueden contener varias lineas de texto. Cuando se determina la fuente del memo,
todo el texto del componente usara la misma fuente, como en el caso del compo-
nente Memo de Delphi. Una vez que se ha escrito el texto deseado, se puede
redimensionar el cuadro Memo, con lo que el texto interior se reorganizara como
corresponda. Si parece que falta texto en el cuadro de Memo despues de haberlo
escrito en el Memo Editor, hay que ajustar el tamaiio del cuadro para que se
pueda ver todo el texto.
El componente Section
El componente Section se utiliza para agrupar componentes, como un Panel en
Delphi. Ofrece ventajas como permitir el desplazamiento de todos 10s componen-
tes que forman parte de la seccion con un solo clic, en lugar de tener que mover
cada componente de forma individual o seleccionar todos 10s componentes antes
del desplazamiento.
El Project Tree resulta util cuando se maneja el componente Section. A partir
de un nodo expandido resulta sencillo comprobar quC componentes se encuentran
en que seccion, ya que 10s componentes compuestos pueden formar un arbol con
relaciones padre-hijo.
- - - -
Componentes graficos
Los componentes Bitmap y Metafile permiten disponer imagenes en un infor-
me. Bitmap soporta archivos de mapas de bits con la extension .bmp, y Metafile
soporta archivos de imagenes vectoriales con las estensiones .w m f y . emf.
Cuando se utiliza Rave en una aplicacion CLX no se soportan 10s componentes
Metafile, porque estan basados en una tecnologia especifica de Windows.
El componente FontMaster
Cada componente Text de un informe tiene un propiedad F o n t . Al establecer
esta propiedad, se puede asignar una fuente especifica al componente. En muchos
casos puede ser util y necesario establecer las mismas propiedades de fuente para
mas de un objeto. Aunque se puede hacer esto si se selecciona mas de un compo-
nente a1 mismo tiempo, este metodo tiene un inconveniente: hay que hacer un
seguimiento de que fuentes deben tener el mismo tipo, tamaiio y estilo, lo que no
resulta sencillo para mas de un informe. El componente FontMaster permite defi-
nir fuentes estandar para distintas partes del informe, como las cabeceras, el
cuerpo y 10s pies de pagina. El FontMaster es un componente no visual (que se
indica mediante el color verde del boton), por lo que no se tendra ninguna referen-
cia visual sobre el en la pagina (no como en Delphi). Al igual que otros compo-
nentes no visuales, solo puede accederse a el mediante el Project Tree.
Una vez que se haya establecido la propiedad F o n t del componente FontMaster,
es sencillo vincularlo con un conjunto de texto. Hay que seleccionar un compo-
nente Test o Memo en el informe y utilizar a continuacion el boton Flecha abajo
que se encuentra junto a la propiedad F o n t M i r r o r en el panel Property para
escoger un enlace con un FontMaster. Cualquier componente cuya propiedad
Fo n t M i r r o r se haya vinculado con el FontMaster se vera afectada por la pro-
piedad F o n t del FontMaster.
Cuando se fija la propiedad F o n t M i r r o r de un componente, se reemplazara
la propiedad F o n t del componente por la propiedad F o n t del FontMaster. Otro
efecto secundario del uso del FontMaster es que se inhabilita la barra de herra-
mientas de fuentes cuando se fija la propiedad FontMirror para ese compo-
nente.
Puede existir mas de un FontMaster por pagina; sin embargo, es muy aconse-
jable renombrar 10s componentes FontMaster para describir su funcion. Tambien
deberian colocarse en una pagina global, para que puedan utilizarse en todos 10s
informes que formen parte de un proyecto y se consiga asi una estructura tipogra-
fica mas consistente.
Nlimeros de pagina
PageNumInit es un componente no visual que permite reiniciar la numeracion
de paginas dentro de un informe. Se utiliza de un mod0 similar a otros componen-
tes no visuales. Lo mas normal es utilizar este componente cuando Sean necesa-
rios formatos mas avanzados .
Por ejemplo, podemos tener en cuenta un informe de estado de cliente para una
cuenta bancaria. Los balances de estado que reciban 10s clientes cada mes pueden
variar en el numero de paginas. Supongamos que la primera pagina define la
estructura de la pagina de resumen de la cuenta, la segunda define 10s creditos o
depositos del cliente, y la tercera define las deudas y las retiradas de fondos.
Puede que 10s dos primeros informes necesiten una sola pagina; per0 si la activi-
dad de la cuenta de un cliente es muy alta, entonces la seccion de retiradas puede
ocupar varias paginas. Si el usuario que genera el informe quiere numerar indivi-
dualmente las paginas de cada seccion, las paginas de resumen y de depositos
deberian marcarse como "1 de 1". Si la cuenta de un cliente activo tiene tres
paginas de retiradas y deudas, esta seccion del balance deberia numerarse como
" 1 de 3", "2 de 3" y "3 de 3 ". PageNumInit es un componente muy practico para
este tipo de numeracion de paginas.
Componentes de dibujo
A1 igual que 10s componentes estandar, 10s componentes de dibujo no tienen
que ver con 10s datos. Los informes de Rave pueden incluir tres tipos de compo-
nentes para lineas: Las lineas genericas se dibujan en cualquier direccion e inch-
yen lineas oblicuas; las lineas horizontales y verticales tienen una direccion fija.
Entre las formas geometricas disponibles hay cuadrados, rectangulos, circunfe-
rencias y elipses.
Se puede dejar caer una forma sobre un informe y despues esconderla tras otro
elemento. Por ejemplo, puede colocarse un rectangulo alrededor de un componen-
te DataBand, ajustar su tamaiio para que ocupe completamente la banda y situar-
lo despuis tras el resto de 10s componentes de la banda.
WdObjscl fW
D d a Lookup Secwly Conlrdln
@ Database Cmnedion
Direct D d a Vlew
a Driver D d a V ~ e w
fl Simple Securny Cordroller
Componente RaveDatabase (conexi6n con la vista de datos): Ofrece
parametros de conexion con una base de datos para el componente
DriverD a t aView (la vista de datos del controlador). Solo se permiten
conexion a bases de datos para las que esten instalados controladores
DataLink.
Componente RaveDirectDataView (vista directa de datos): Proporcio-
na un medio de obtener datos desde un componente de conexion de datos
que se encuentre en la aplicacion Delphi anfitriona, como en el ejemplo
anterior. (La seleccion se llama vista directa de datos o Direct Data View
incluso aunque no exista la conexion directa con la base de datos desde el
informe, sino que se trate de la conexion indirecta basada en datos extrai-
dos de la base de datos de una aplicacion anfitrion.)
Componente RaveDriverDataView (vista de datos de controlador):
Proporciona un mod0 de definir una consulta para una conexion de base de
datos especifica mediante un lenguaje de consultas como SQL. Se mostra-
ra un constructor de consultas para definir la consulta.
Componente RaveSimpleSecurity (controlador de seguridad simple):
Implementa la forma mas basica de seguridad mediante el empleo de una
sencilla lista de pares de nombre de usuario y contraseiia en la propiedad
US erList . user Lis t contiene un par de nombre de usuario y contra-
seiia por linea, de acuerdo con este formato: username = password.
Ca seMa t ter s es una propiedad booleana que controla si la contraseiia
tiene en cuenta las mayusculas.
Componente RaveLookupSecurity (seguridad de busqueda de datos):
Permite verificar 10s pares de identificacion mediante entradas en una ta-
bla de base de datos. La propiedad Dat aview especifica la vista de datos
a utilizar para buscar el nombre de usuario y la contraseiia. Las propieda-
des UserField y PasswordField se usan para buscar el nombre de
usuario y la contraseiia que se deben verificar.
Regiones y bandas
Un componente Region es un contenedor de componentes Band. En su forma
mas simple, la region podria ser todo el componente Page. Esto seria cierto para
un informe que sea un tipo lista. Muchos informes maestro-detalle podrian enca-
jar en un diseiio de una unica region. Sin embargo, no hay que limitarse a pensar
en una region como en toda la pagina; las propiedades de una region tienen que
ver con su tamaiio y posicion en la pagina. El uso creativo de las regiones ofrece
mas flexibilidad cuando se diseiian informes complejos. Se pueden colocar varias
regiones en una unica pagina; pueden estar adosados, apilados, o distribuidos por
la pagina.
TRUCO: N o hay que confundir una region (Region) con una seccion
(Section). Los componentes Region solo pueden contener componentes Band.
Un componente Section puede contener cualquier grupo de componentes,
como cimponentes ~ e ~ i oper0
n , no directameite componentes ~ & d .
Cuando se trabaja con bandas hay que seguir una regla muy sencilla: Las
bandas tienen que estar en una region. Hay que tener en cuenta que no hay limite
a1 numero de regiones en una pagina ni a1 numero de bandas en una region.
Mientras se puede ver mentalmente el informe, se puede usar una combinacion de
regiones y bandas para resolver cualquier dificultad que surja a la hora de plas-
mar esas ideas en forma de diseiio.
Esisten dos tipos de banda:
DataBand: Se usan para mostrar informacion repetitiva procedente de una
vista de datos. En general, un componente DataBand contendra varios com-
ponentes DataTest. La propiedad D a t a V i e w de un DataBand debe fijar-
se a1 componente DataView (la vista de datos) sobre la que habra que
realizar las repeticiones, y tipicamente contendra otros componentes data-
aware que trabajaran con la misma D a t a V i e w .
Band: Se usa para mostrar bandas de cabecera y pie de pagina en una
region. Entre 10s tipos soportados estan Body, Group y Row: se escogen
mediante la propiedad B a n d S t y l e . No se necesitan que las cabeceras o
pies de pagina esten en una banda porque se pueden dejar caer directamen-
te sobre la pagina, fuera del componente Region.
Una propiedad importante del componente Band es C o n t r o 1l e r B a n d . Esta
propiedad determina a que DataBand pertenece un componente Band (o por que
componente esta controlado).
Cuando se establece la DataBand de control, hay que fijarse en que el simbolo
grafico de la banda apunta en la direccion de su controlador y que 10s colores de
10s simbolos se corresponden. A continuacion explicaremos 10s codigos de letras
que se muestran en la banda.
Cornponentes data-aware
Se pueden colocar distintos componentes data-aware de Rave en un DataBand.
La opcion mas comun es el componente DataText, utilizado para mostrar un
campo de texto procedente de un conjunto de datos como se muestra en el ejemplo
Ravesingle.
Hay disponibles dos opciones para introducir datos en una propiedad
D a t a F i e l d . La primera es seleccionar un unico campo mediante la lista desple-
gable; este enfoque esta bien para informes normales en que solo se necesite un
campo de datos para cada elemento DataText. La segunda es utilizar el Data Text
Editor.
El Data Text Editor
Muchos informes necesitan combinar varios campos. Dos ejemplos bastante
comunes son las combinaciones de ciudad, provincia y codigo postal, o de nombre
y apellido. En el codigo, se pueden realizar estas combinaciones mediante senten-
cias como las siguientes:
City + ' , ' + State + ' ' + Zip
FirstName & LastName
--
I W a Ted --
- - - --
1 FIRST-NAME
De Text a Memo
El componente DataMemo muestra un campo de memo procedente de una
Dataview. La diferencia principal entre el componente DataMemo y el compo-
nente DataTest es que el primer0 se utiliza para mostrar texto que necesita mas de
una linea y necesita adaptar su forma. Por ejemplo, podria utilizarse para impri-
mir comentarios sobre un cliente en la parte inferior de cada pagina de una factu-
ra.
Un posible uso para el componente DataMemo son las funciones de fusion de
correos. El mod0 mas sencillo de realizar esto es establecer las propiedades
DataView y DataField como la fuente del campo Memo. Despues, se arran-
ca el Mail Merge Editor haciendo clic sobre el boton de puntos suspensivos que se
encuentra junto a la propiedad Mai 1MergeI tems.Este editor permite determi-
nar 10s elementos del Memo que se modificaran.
Para usar el Mail Merge Editor, hay que hacer clic sobre el boton Add. En la
ventana Search Token, se escribe el elemento que esta en el Memo y que sera
sustituido. Despues, se escribe la cadena de sustitucion en la ventana Replacement
o se hace clic sobre el boton Edit para arrancar el Data Text Editor que ayudara
a seleccionar las distintas vistas de datos y campos.
Calculo de totales
El componente CalcText es un componente de recuentodata-aware. La mayor
diferencia entre el componente DataText y el componente CalcText es que
este ultimo esta diseiiado especialmente para realizar calculos y mostrar 10s resul-
tados. La propiedad CalcType determina el tipo de calculo que se va a realizar,
como promedio (Average), recuento (Count), maximo (Maximum), minimo
(Minimum) y sumatorio (Sum). Por ejemplo, se puede utilizar este componente
para imprimir 10s totales de una factura en la parte superior de cada pagina.
La propiedad CountBlanks determina si 10s valores de 10s campos vacios
se incluyen en 10s metodos de calculo de recuento y promedio. Si RunningTotal
es True,entonces no se volvera a poner a 0 el valor del calculo cada vez que se
imprima.
Rave avanzado
En esta extensa introduccion de Rave hemos visto que este sistema de genera-
cion de informes es tan complejo que podria dedicarsele todo un libro. Ya hemos
creado algunos ejemplos, y podriamos continuar mostrando una relacion maestro-
detalle u otros informes con una estructura compleja. Sin embargo, con 10s asis-
tentes y la informacion disponible hasta ahora, deberian poderse hacer ejemplos
similares sin gran dificultad. Por esto, en esta seccion solo vamos a crear un unico
ejemplo de este tipo, y despues ofreceremos informacion sobre unos cuantos as-
pectos importantes de Rave que no son faciles de entender mediante el procedi-
miento de prueba y error.
NOTA: Entre Ias caracteristicas de Rave que no vamos a comentar aqui
esth una que permite distribuir el disefiador de informes a 10s usuarios
r---i-- I ---- -----I&:-
irnales (para ---- pc;rsonanc;en
pcrmlur qut: I: --.-I - - L C ------
\
10s mrorrntts), --- --I---J-
ya sea ---
enlazaoo con
un programa escrito en Delphi o como herramienta independiente. Rave
tambien tiene una version de servidor que permite obtener informes de un
servidor Web.
lnformes maestro-detalle
Para crear un informe maestro-detalle en Rave, se necesitan dos conjuntos de
datos en la correspondiente aplicacion Delphi, per0 no es necesario que estos
conjuntos de datos tengan definida una relacion maestro-detalle en el programa,
ya que el propio informe puede definir una relacion de este tipo. En el programa
de muestra RaveDetails, se expone cada uno de 10s conjuntos de datos a traves de
una conexion Rave:
o b j e c t dsDepartments: TSimpleDataSet
Connection = SQLConnectionl
DataSet. CommandText = ' select * from DEPARTMENT'
end
o b j e c t dsEmployee: TSimpleDataSet
Connection = SQLConnectionl
DataSet.CommandText = 'select * from EMPLOYEE'
end
o b j e c t RvConnectionDepartments: TRvDataSetConnection
DataSet = dsDepartments
end
o b j e c t RvConnectionEmployee: TRvDataSetConnection
DataSet = dsEmployee
end
1v oasv*lrRegm -W1-
Simple T a b d l
Figura 18.6. El informe maestro-detalle. El Band Style Editor aparece por delante.
Guiones de informes
A1 comienzo de este capitulo hablamos de la ventana Event Editor del Rave
Designer, per0 aun no lo hemos usado. Esta herramienta se utiliza para escribir
codigo (guiones o scripts) en un informe, que responda a eventos de 10s diversos
componentes, como se haria en Delphi. La escritura de guiones en el Rave
Designer permite personalizar o ajustar el resultado de un informe de un mod0
muy sofisticado. El lenguaje que utiliza Rave para 10s guiones se basa en Pascal
y es una variante del lenguaje Delphi, por lo que no deberia haber mucho proble-
ma en su comprension.
El ejemplo RaveDetails muestra en negrita 10s sueldos que son mayores que
una cantidad cspecificada. El mod0 mas obvio de realizar esto es escribir un
codigo interpretado que se ejecute para cada instancia de la banda detalle (es
decir, para cada registro dc la base de datos de empleados). En lugar de modificar
directamente la propiedad Font, hemos decidido aiiadir dos componentes
FontManager distintos a la pagina del informe y cambiarles el nombre para que sc
comprenda su funcion: fmPlain Font y fmBold Font. Se puede abrir el in-
forme para vcr sus propiedades y estructura.
En cl informe, para resaltar 10s valores que se encuentrcn por encima de un
rango dado, se controla el evento Bef orePr int del componente Da t a T e x t .
Para cllo, iremos a la pagina Event Editor, escogeremos el componente DataText
conectado con el campo Salary y escogeremos el evento. En la ventana de edicion
de codigo dcl evento, escribiremos este codigo:
if DataView2Salary.AsFloat > 1 0 0 0 0 0 then
self.FontMirror : = fmBoldFont;
else
self.FontMirror := fmPlainFont;
end if;
Espejos
Las plantillas de informes pueden tener uno o mas componentes y reutilizarse
mediante la tecnologia de espejo de Rave. El componente DataMirrorSection re-
fleja otras secciones de acuerdo con 10s contenidos de un DataField. El uso de
secciones espejo permite que la DataMirrorSection sea muy flexible. Conviene
recordar que las secciones pueden contener cualquier otro componente como gra-
ficos, regiones, texto y demas.
Por ejemplo, podria usarse un componente DataMirrorSection para que un
unico informe genere distintos formatos de sobre para direcciones internacionales
o nacionales. La plantilla para 10s usuarios internacionales podria incluir una
linea para el pais con el texto centrado en el sobre, mientras que el formato
nacional podria no incluir la linea de pais y podria tener el testo en la parte
inferior derecha del sobre.
Corporate Headquarters
BudgetCjalary
1 000.000.00
LOCATION
Monterey
Lee, Tern 53 793.00
Bender, Ollver H. 212.850.00
I Engineering
Nelson, Robelt
1.100.000.(
105.900.0
I
age 1 o f 3
Brown, Kelly
-
-
270M,C
Calculos a tope
Ademas del sencillo componente CalcTest ya comentado, el Rave Designer
incluye tres componentes para manejar situaciones mas complejas: CalcTotal,
CalcController y CalcOp.
CalcTotal
El componente CalcTot a1 es una version no visual del componente
CalcText.Cuando se imprime este componente, lo m h habitual es guardar su
valor en un parametro de proyecto (definido por la propiedad ~estParam) y darle
formato de acuerdo con la propiedad DisplayFormat.Puede resultar util cuando
se realicen calculos totales que se utilizaran en otros calculos antes de presentarse.
Si el valor del CalcTotal solo se va a usar en otros componentes de calculo, como
CalcOp, deberia dejarse en blanco la propiedad De stParam.
CalcController
C a 1cCont ro 11e r es un componente no visual que actua como un contro-
lador para 10s componentes CalcText y CalcTo tal mediante sus propieda-
des contro 1ler. Cuando se imprime el componente controlador, indica a todos
10s componentes de calculo que controla que realicen sus operaciones. Este proce-
so permite que un informe vuelva a calcular 10s totales de de bandas de grupo, de
detalle o de paginas completas segun cual sea la situacion del componente
CalcController.
El componente CalcController tambien puede iniciar un componente CalcText
o CalcTotal con un valor especifico (mediante las propiedades Initcalcvar,
InitDataField e Initvalue). El componente CalcController solo
iniciara 10s valores si se usa en la propiedad Init ia1izer de 10s componentes
CalcTexto CalcTotal.
CalcOp
CalcOp es un componente no visual que permite realizar una operacion (defi-
nida por la propiedad Ope rator) sobre valores de distintas fuentes de datos. El
resultado puede guardarse despuQ de un parametro de proyecto, como CalcTotal,
tal y como indiquen las propiedades De stParam y DisplayFormat.
Por ejemplo, supongamos que necesitamos aiiadir dos componentes DataText,
como en A + B = C (donde A y B representan 10s valores de dos componentes
DataText y c representa el resultado que se almacena en un parametro de proyec-
to). Los tres tipos de fuentes tienen asociados muchos valores distintos.
El calculo puede comenzar con distintos tipos de fuentes de datos:
Una fuente Data Fie ld es un campo en una tabla, o un DataView en
terminos de Rave. Por ello, para escoger un campo hay que seleccionar
antes un Dataview.
Para una fuente Value, se rellenara la propiedad con un valor numerico.
Una fuente Calcvar representa otra variable de calculo; se puede esco-
ger una del menu desplegable que muestra la lista de variables de calculo
disponibles en la pagina. Este valor puede proceder de otro componente
CalcOp o de algun otro componente de calculo.
Despues de escoger las fuentes de datos, hay que seleccionar la operacion que
se realizara con ellos. La propiedad operator tiene un menu desplegable que
puede usarse para realizar la eleccion adecuada. En el ejemplo A + B = C, el
operador es c o ~ d d .
En ocasiones se necesita realizar una funcion con un valor antes de procesarlo
junto con el segundo valor. En este caso es practica la propiedad Function de
la fuente. Con una funcion se puede convertir un valor (corno de horas a minutos),
calcular una funcion trigonometrica (corno el sen0 de un valor) o realizar muchos
otros calculos (corno una raiz cuadrada o el valor absoluto).
Es tan importante asegurarse de que 10s componente estan ordenados en el
Project T r e e para realizar 10s calculos en orden. Un informe ejecuta 10s compo-
nentes en sentido descendente en el Project Tree. Para 10s componentes CalcOp
o cualquier otro componente de calculo, esto significa que deben seguir el orden
correcto. Tambien es importante tener en cuenta que si un valor de fuente depende
de otro componente (corno de otros componentes C a l c O p o componentes
DataText), esos componentes deben encontrarse antes en el Project Tree.
Parte IV
Delphi e Internet
Programacion
para Internet:
sockets e lndy
eso, cualquier operaci6n con sockets en clientes que pueda ser de gran
duracion, deberia Ilevarse a cab0 dentro de una hebra o empIeando el com-
ponente IdAntiFreeze de Indy como una alternativa mas simple pero tam-
bien limitada. El uso de conexiones bloqueantes para implementar un
protocolo tiene la ventaja de simplificar la logica del programa, ya que no - -
es necesario utilizar
uti la tecnica de mdquina dekstados be las conexiones no
bloqueantes.
T'odos 10s servidores
servi Indy utilizan una arquitectura multihilo que se puede
--- I10s -- --------- T A - ~ L - - - A ~ ~ - - ~ - L - - I ~- r > - r t - - - A n # - - n - - i
conirolar con
- - - A - - ~ - -
cornponentes la I nreaalvlgrueraulr e IU I nreaamgrrool.
A--
Puertos TCP
Cada conexion TCP se realiza a traves de un puerto, que se representa como
un numero de 16 bits. La direccion IP y el puerto TCP especifican juntos una
conexion de Internet o un socket. Distintos procesos activos en la misma maquina
no pueden utilizar el mismo socket (el mismo puerto).
Algunos puertos TCP tienen un uso estandar para protocolos y servicios de
alto nivel especificos. En otras palabras, deberian utilizarse esos numeros de
puerto cuando se implementen esos servicios y no utilizarlos en ningun otro caso.
Esta es una breve lista:
Ahora que se ha establecido una conexion, se necesita hacer que 10s dos pro-
gramas se comuniquen. Tanto 10s sockets de cliente como de servidor tienen me-
todos de lectura y escritura que pueden utilizar para enviar datos, per0 escribir un
servidor multihilo que pueda recibir muchas ordenes distintas (normalmente ba-
sadas en cadenas) y trabajar de distinto mod0 con cada una de ellas no es algo
trivial. Sin embargo, Indy simplifica el desarrollo de un servidor mediante su
arquitectura de comandos. En un servidor se puede definir un cierto numero de
comandos, que se guardan en la coleccion CommandHandlers de IdTCPSewer.
En el ejemplo IndySockl el servidor tiene tres controladores, todos ellos
implementados de distinta manera para mostrar algunas de las posibles alternati-
vas. La primera orden de servidor, llamada test,es la mas simple, porque esta
completamente definida en sus propiedades. Hemos preparado la cadena de la
orden, un codigo numeric0 y una cadena resultante en la propiedad ReplyNorma1
del controlador de ordenes:
object IdTCPServerl: TIdTCPServer
CommandHandlers = <
i tern
Command = ' t e s t '
Name = ' T I d C o r r u n a n d H a n d l e r O '
Parseparams = False
ReplyNormal.NumericCode = 100
ReplyNorma1.Text.Strings = (
'Hello from your Indy Server')
ReplyNormal.TextCode = '10 0 '
end
El programa cliente que hemos creado trabaja con un ClientDataSet con esta
estructura guardada en el directorio actual. (Se puede ver el codigo pertinente en
el controlador del evento oncreate.) El metodo principal en el cliente es el
controlador del evento OnClic k del boton Send All, que envia todos 10s nuevos
registros a1 servidor. Se ve que un registro es nuevo fijandose en si el registro
tiene un valor valido para el campo CompID. Este campo no lo establece el usua-
rio, si no la aplicacion servidor cuando se envian 10s datos.
Para todos 10s registros nuevos, el programa cliente empaqueta la informacion
de campos en una lista de cadenas, utilizando la estructura F i e l d N a m e =
Fie ldValue.La cadena correspondiente a la lista completa, que es un registro,
se envia entonces a1 servidor. En este punto, el programa espera a que el servidor
envie de vuelta el identificador de la empresa, que se guarda entonces en el regis-
tro actual. Todo este codigo se ejecuta en una hebra, para evitar bloquear la
interfaz de usuario durante operaciones pesadas. A1 hacer clic sobre el boton
Send, un usuario lanza una nueva hebra:
procedure TForml.btnSendClick(Sender: TObject);
var
SendThread: TSendThread;
begin
SendThread : = TSendThread.Create(cds);
SendThread.OnLog : = OnLog;
SendThread.ServerAddress : = EditServer.Text;
SendThread.Resume;
end :
Excepto por el hecho de que podrian perderse algunos datos, no hay ningun
problema cuando 10s campos tienen un orden distinto y si no se corresponden, ya
que 10s datos se guardan con la estructura FieldName=FieldValue. Des-
pues de recibir todos 10s datos y enviarlos a la tabla local, el servidor envia de
vuelta el identificador de la empresa a1 cliente. Cuando se recibe esta respuesta, el
programa cliente guarda el identificador de la empresa, con lo que el registro se
marca como enviado. Si el usuario modifica este registro, no existe ningun mod0
de enviar una actualizacion a1 servidor. Para conseguir esto, podria aiiadirse un
campo modificado a la tabla de la base de datos de cliente y hacer que el servidor
comprobase si esta recibiendo un campo nuevo o un campo modificado. Con un
campo modificado, el servidor no deberia aiiadir un nuevo registro, sin0 actuali-
zar el existente.
Como muestra la figura 19.2, el programa servidor tiene dos paginas: una con
un registro y otra como un DBGrid que muestra 10s datos actuales en la tabla de
la base de datos del servidor. El programa cliente es una entrada de datos basada
en formularios, con botones adicionales para enviar 10s datos y eliminar 10s regis-
tros ya enviados (y para 10s cuales se haya recibido un identificador).
Addless
l~lmanza
Stale cuml~aunlly
: 1 plaly
I Emd
I rnarco@marcocanluc a n
, Canla3
Marco Canlu
Figura 19.2. Los programas cliente y servidor del ejemplo de sockets de base de
datos (IndyDbSock).
Otro interesante ejemplo del uso del correo es informar a 10s desarrolladores
de problemas de las aplicaciones (una tecnica que podria desearse utilizar en una
aplicacion interna en lugar de en una distribuida para el publico). Se puede conse-
guir este efecto modificando el ejemplo ErrorLog del capitulo 2 para enviar co-
rreo cuando se produzca una excepcion (o solo una de un tip0 dado).
Figura 19.3. El programa SendList en tiempo de diseiio.
unit FindTh;
interface
uses
Classes, Idcomponent, SysUtils, IdHTTP;
type
TFindWebThread = class (TThread)
protected
Addr, Text, Status : string;
procedure Execute; override;
procedure AddToList;
procedure ShowStatus;
procedure GrabHtml;
procedure HtmlToList;
procedure HttpWork (Sender: TObject; AWorkMode:
TWorkMode;
const AWorkCount: Integer);
public
strUrl: string;
strRead: string;
end;
implementation
uses
WebFindF;
procedure TFindWebThread.AddToList;
begin
if Forml. ListBoxl. Items.Indexof (Addr) < 0 then
begin
Forml.ListBoxl.1tems.Add (Addr);
Forml.DetailsList.Add (Text);
end;
end;
procedure TFindWebThread-Execute;
begin
GrabHtml;
HtmlToList;
Status : = 'Done with ' + StrUrl;
Synchronize (ShowStatus);
end ;
procedure TFindWebThread-GrabHtml;
var
Httpl: TIdHTTP;
begin
Status := 'Sending query: ' + StrUrl;
Synchronize (ShowStatus);
Httpl : = TIdHTTP-Create (nil);
try
Http1.Request.UserAgent : = 'User-Agent: NULL';
Httpl.OnWork : = HttpWork;
strRead : = Httpl .Get (StrUrl);
finally
Httpl.Free;
end;
end;
procedure TFindWebThread.Htm1ToList;
var
strAddr, strText: string;
nText : integer;
nBegin, nEnd: Integer;
begin
Status := ' E x t r a c t i n g d a t a f o r : ' + StrUrl;
Synchronize (ShowStatus);
strRead : = Lowercase (strRead);
repeat
// e n c u e n t r a l a p a r t e i n i c i a l d e l a r e f e r e n c i a HTTP
nBegin : = Pos ( ' h r e f = h t t p r , strRead) ;
if nBegin <> 0 then
begin
// o b t i e n e l a p a r t e r e s t a n t e d e l a c a d e n a , d e s d e h t t p
strRead : = Copy (strRead, nBegin + 5, 1000000);
/ / e n c u e n t r a e l f i n a l d e l a r e f e r e n c i a HTTP
nEnd : = Pos ( ' > I , strRead) ;
strAddr : = Copy (strRead, 1, nEnd - 1);
// c o n t i n u a
strRead : = Copy (strRead, nEnd + 1, 1000000) ;
/ / a f i a d e e l URL s i n o s e e n c u e n t r a g o o g l e
if Pos ( ' g o o g l e ' , strAddr) = 0 then
begin
nText : = Pos ( ' < / a > ', strRead) ;
strText : = copy (strRead, 1, nText - 1);
// e l e m i n a l a s r e f e r e n c i a s y d u p l i c a d o s d e l a c a c h e
if (Pos ( ' c a c h e d ' , strText) = 0) then
begin
Addr : = strAddr;
Text : = strText;
AddToList;
end;
end;
end;
until nBegin = 0;
end;
procedure TFindWebThread.AddToList;
begin
Forml.StatusBarl.SimpleText : = Status;
end;
end.
El programa busca apariciones consecutivas de la cadena href=http, co-
piando el testo que sigue hasta el caracter de cierre >. Si la cadena encontrada
contienc la palabra google o el testo incluye la palabra cached,se ignora. La
figura 19.4 muestra el efecto de este codigo. Se pueden iniciar varias busquedas
a1 mismo tiempo, per0 hay que tener en cuenta que 10s resultados se aiiadiran a1
mismo componente de memo
Figura 19.4. La aplicacion WebFind puede utilizarse para buscar una lista de sitios
en el motor de busqueda de Google.
La API Winlnet
Cuando se necesita utilizar 10s protocolos FTP y HTTP, como alternativas a1
uso de componentes particulares de la VCl, se puede usar una API especifica de
Microsoft, que se ofrece en la DLL WinInet. Esta biblioteca forma parte del
nucleo del sistema operativo e implementa 10s protocolos FTP y HTTP sobre la
API de sockets de Windows.
Con solo tres llamadas (Internetopen, InternetOperURL e
InternetRead Fi le) se puede conseguir un archivo que se corresponda con
cualquier URL y guardarlo como una copia local o analizarlo. Se pueden utilizar
otros metodos simples para FTP; es aconsejable analizar el codigo fuente de la
unidad Delphi win1 net .pas,que ofrece una lista de todas las funciones.
Un navegador propio
Aunque no es probable que interese escribir un nuevo navegador Web, podria
resultar interesante comprobar como se puede conseguir un archivo HTML de
Internet y mostrarlo localmente, empleando el visor HTML disponible en CLX (el
control TextBrowser). Si se conecta este control a un cliente HTTP de Indy, se
puede conseguir rapidamente un navegador Web de texto con unas capacidades
de navegacion limitadas. La base es:
TextBrowserl.Text := 1dHttpl.Get (NewUrl);
11
l-t 8lh. 2 m Ths sde has been moved to a Li-
problems here and there w t h case inc~sislencies.If you
w c l c a r e or lowncasc the lir* IeUer, a email me with the problem..
4 1
I. . . :.....
.. .T,. .,. ...
-- -
Goto:
Una vez mas, este ejemplo es trivial y poco funcional, per0 construir un
navegador implica poco mas que la capacidad de conectarse mediante HTTP y
mostrar archivos HTML.
' w
-
['d
I http ,,localhost 8080/1e.
I HttpSen-Demo
Recargar
-
Inca
Request /test
Si este ejemplo parece demasiado trivial, se podra ver una version algo mas
interesante en la prosima seccion, en la que hablaremos de la generacion de codi-
go HTML con 10s componentes productores de Delphi.
- - . ..
NOTA: Si se tiene planeado crear un servidor Web avanzado u otros servi-
dores de Internet con Delphi, entonces, como alternativa a 10s componentes
Indy, deberian consultarse 10s componentes DXSock de Brain Patchwork
DX (www.dxsock.com).
Generacion de HTML
El lenguaje de marcas de hipertexto, HTML, es el formato mas extendido para
distribuir contenido en la Web. HTML es el formato que tipicamente leen 10s
navegadores Web; es un estandar definido por el W3C (World Wide Web
Consortium, www.w3.org), que es uno de 10s organismos que controlan Internet.
El documento del HTML estandar esta disponible en www.w3 .org/markUp. junto
con algunos enlaces bastante interesantes.
thlmb
t head,
-
*
<l~lle>P~oducerDunocJt~llel
</head>
<body,
thl>Producer Democ/hl>
<p>Thisis a demo d the page producedby the
tb>HtmlP~odexe</b> apphcation on <b>12/4/2002t/b>.</p>
<hr,
tp>The prices in lhis catalog are valrd unld
tb>12/25RWZclb>.<lp>
</body>
Figura 19.7. El resultado del ejemplo HtmlProd, una sencilla demostracion del
componente PageProducer, cuando el usuario hace clic sobre el boton Demo Page.
A1 usar etiquetas con 10s nombres de 10s campos del conjunto de datos conec-
tado (la tipica tabla de la base de datos COUNTRY.DB), el programa obtendra
automaticamente el wlor de 10s campos del registro actual y 10s sustituira instan-
taneamente. Esto produce la salida quc muestra la figura 19.8; el navegador esta
concctado a1 ejemplo HtmlProd que funciona como un servidor HTTP, como
veremos mas adelante. En el codigo fuente del programa relacionado con este
componente, no esiste ninguna referencia a 10s datos de la base de datos:
p r o c e d u r e TFormProd.DataSetPageProducerlHTMLTag(Sender:
TObject; Tag: TTag;
c o n s t TagString: String; TagParams : TStrings; v a r
ReplaceText: String);
begin
i f TagString = 'program' then
ReplaceText : = ExtractFilename (Forms.Application.Exename)
else i f T a g s t r i n g = ' d a t e ' then
ReplaceText : = DateToStr ( D a t e ) ;
end;
Figura 19.8. El resultado del ejemplo HtmlProd para el boton Print Line.
DataSetTableProducer Demo
II American Clountries
El resto del codigo esta resumido en 10s parametros del componente productor
de la tabla, incluidos su cabecera y pie, como puede verse si se abre el codigo
fuente del ejemplo HtmlProd.
Por ultimo, si el programa devuelve una tabla que utiliza CSS, el navegador
solicitara el archivo CSS a1 servidor; por eso hemos afiadido algo de codigo
especifico para devolverlo. Con las apropiadas generalizaciones, este codigo mues-
tra como puede responder un servidor devolviendo archivos, y tambien como
indicar el tipo MIME de la respuesta ( C o n t e n t T y p e ) :
if Pos ( ' t e s t . c s s f , Req) > 0 then
begin
CssTest : = TStringList.Create;
try
CssTest.LoadFromFile(ExtractFi~ePath(Applicat~on.ExeName)
+ 'test.cssl);
Response1nfo.ContentText : = CssTest.Text;
ResponseInfo.ContentType : = 'text/css';
finally
CssTest.Free;
end;
Exit;
end ;