Sunteți pe pagina 1din 32

Programando tres capas en Visual Foxpro

Aunque nunca antes haya escrito una aplicacin de 3 capas en Foxpro, ha odo hablar
ciertamente de ellas. Cul es la gran idea? Es algo que necesitamos aprender?
Publicado por msalias el viernes, 12 de marzo de 2004

En este artculo, descubrir que (1) necesita aprenderlo absolutamente,


(2) es fcil, y (3) la biblioteca que incluyo puede usarla en su prximo
proyecto.

ste es el primero de una serie de dos artculos sobre el desarrollo de 3


Les Pinter capas. En este, construiremos la capa de acceso a datos para
comunicarnos con DBFs o un Servidor SQL. Lo contruiremos de forma
tal que que no necesitemos cambiar el cdigo cuando movamos nuestros datos de DBFs
a tablas de SQL. Incluiremos adems un asistente para el convertidor, incluso para que
transfiera datos por nosotros. Hablaremos sobre las cosas que no quiere hacer en SQL si
quiere simplificar la programacin (siempre una cosa buena); y pavimentaremos el
camino para la 2da parte de la serie, donde podr desarrollar habilidades a travs de
WebConnection o usar servicios de XML contruidos sobre VFP 8 para una mejor
optimizacin. El cdigo para este artculo se escribe para ser compatible con VFP 7,
mientras el cdigo para el prximo artculo trabajar con cualquier versin.

Por qu 3 capas?

Tres-Capas es una variante de n-Capas. A llama a B, B llama a C, etc. Cada una hace su
parte de la tarea. Con familias de servidores y aplicaciones ASP, pueden existir
diferentes capas de datos, una capa para generar pginas, y muchas ms. Para nuestros
propsitos, tres-capas es generalmente suficiente. En los diagramas de tres-capas
usuales (qu distribuiremos aqu), A es un formulario, B es una capa de acceso a datos,
y C es el lugar dnde guardamos los datos - normalmente DBFs en nuestro mundo, pero
eso est cambiando, y es ah donde entra nuestra capa de acceso a datos.

En las aplicaciones de FoxPro tradicionales, nuestros formularios contienen el cdigo


que extrae y guarda los datos. Nuestro cdigo est lleno de rdenes SEEK y REPLACE.
El problema se origina cuando nuestro clientes deciden que estn cansados de dar
puntapis a todos fuera de la aplicacin para reconstruir los ndices, o restablecer un
backup de 400 archivos que simplemente fall porque un usuario no cerr el archivo de
CONTROL, u observar como un APPEND BLANK demora treinta segundos porque la
tabla tiene 900,000 artculos y el ndice es de 9MB. las DBFs son grandes, pero tienen
sus desventajas. La solucin se deletrea S-Q-L.

Existen numerosas ventajas. Cuando salvamos una Base de datos de SQL, estamos
guardando un solo archivo. La salva puede ejecutarse mientras los usuarios estn en el
sistema. Y restaurar es un comando de una lnea.

La seguridad es otro problema con las tablas de FoxPro. Cualquiera con acceso al
directorio de las DBFs en el servidor puede ver nuestras tablas. El Servidor de SQL, por
otro lado, tiene la seguridad incorporada. As que puede decidir quin lo hace. Cada da
que pasa el riesgo aumenta y los usuarios conocen y exigen mejoras de seguridad. El
servidor de SQL es una buena manera de lograrlo.
S su cliente vende. Instale el SQL. Puede ejecutar el SQL simplemente en su mquina
de desarrollo; de hecho, es una gran idea. Asegrese de instalar Developer Edition que
tiene una consola de administracin. Si todo lo que posee es MSDE, funcionar bien,
pero tendr que crear la base de datos, ndices e inicios de sesin programticamente, y
simplemente es un poco ms difcil aprender las tareas de SQL de forma no visual.

El Servidor de SQL corre como un servicio. "Escucha" las peticiones de las estaciones,
hace lo que se le pregunta, y enva el resultado de vuelta a la estacin. No hay trfico
del ndice porque los ndices no regresan con los resultados. La mayora del retraso que
podramos experimentar con aplicaciones de Foxpro sobre LAN es debido al trfico de
la red, resolver solamente el problema del retraso es una motivacin suficiente para
emigrar al Servidor de SQL.

Si ya hemos instalado el Servidor de SQL, necesitamos pasar la aplicacin para usar las
tablas del Servidor de SQL. Primero, tenemos que pasar los datos. Hay varias maneras
de hacer esto, y todas presentan problemas. Podemos usar el Asistente de Upsizing de
SQL para hacerlo, pero las tablas del SQL resultantes pueden causar dolores de cabeza
de programacin serios. Existe una utilidad de DTS que se instala cuando usted carga el
Servidor del SQL, y si le gusta escribir sus rutinas de recodificacin en BASIC,
conseguir lo correcto. Yo prefiero FoxPro. As que si su mejor apuesta es escribir su
propio programa de migracin de datos. No se preocupe - es incluido en este artculo.

Donde radican los errores con el Asistente de Upsizing? En primer lugar, tiene como
valor predefinido permitir NULLs. Si nunca ha odo hablar de NULLs, seguro no le van
a gustar. Estadsticos necesitan conocer si un valor de cero es un valor informado o es
simplemente alguien que no contest la pregunta - por ejemplo, su edad? no podemos
calcular la media de la edad sumando las edades y dividiendo por cero si la mitad del
encuestados no quiso contestar. As que tiene que saber qu son los "valores perdidos".
El Servidor del SQL permite los valores perdidos, y de hecho los valores
predeterminados a ellos. Pero ellos casi doblan la carga de la programacin. ASP pierde
el control con los valores NULLS y es necesario invertir mucho esfuerzo para lograr
algo. A menos que los necesite realmente, de verdad preocpese de los valores perdidos,
no querremos usar NULLs absolutamente.

Por eso la manera preferida de declarar una columna en el T-SQL es

Age Integer NOT NULL DEFAULT 0,...


Le aconsejo insistentemente que incluya NOT NULL en sus declaraciones de columna,
y proporcione un valor predefinido.

En segundo lugar, el SQL tiene palabras reservadas que tienen que ser directamente
encerradas entre parntesis si las usa como nombres de campo. Yo no las uso porque
tengo mis mtodos. Pero a menudo tenemos que sustentar la herencia de nuestras
aplicaciones y eso significa usar dos sistemas en paralelo por un perodo de tiempo. Por
lo que si necesitramos usar una palabra reservada como nombre de columna
deberamos encerrarla entre parntesis mientras contruyamos la definicin de la tabla.
El programa de conversin de datos le proporciona una lista de palabras claves de SQL
que haya usado como los nombres de los campos. Muestro la lista con los sospechosos
habituales.
Finalmente, para hacer nuestra actualizacin ms fcil, es una buena idea mantener un
nmero entero como ndice primario para cada tabla, sobre todo si tenemos otros
campos de texto que hemos estado usando como llaves secundarias. La razn es que esa
llave nica es esencial si queremos hacer la codificacin de actualizacin lo ms simple
posible, puesto que cada tabla debe de tener uno. En el mundo de FoxPro hemos
desarrollado el mal hbito de usar un ndice compuesto (por ejemplo PONum+LineNum
para guardar los artculos de las ordenes de compra) que simplemente es una pesadilla
para codificar en alguna moda genrica.

El Servidor del SQL ofrece una facilidad de autoincremento llamada IDENTITY. Por
ejemplo, puede declarar un campo entero nombrado MyKey y luego IDENTITY(1,1)
(empieza con 1 e incrementa por 1) en, y cada vez que INSERTE un registro un nuevo
valor aparecer. El problema es que si necesita el acceso instantneo a ese valor,
necesita agregar el comando SELECTO @@IDENTITY despus de la INSERCIN
(por ejemplo, despus de agregar un ttulo de Factura y antes de insertar las Lneas de
Detalle relacionadas con la Factura) para proporcionar el ndice del ttulo en el detalle
para unirlas subsecuente. Y hay otras razones. Si tiene un campo IDENTITY, no debe
incluir su nombre en cualquier orden de INSERT. As que su cdigo de actualizacin
tiene que saber saltar el campo si el mismo es un campo IDENTITY.

Imagnelo. Haremos nuestra propia generacin de ndice primario en lugar de usar la


facilidad IDENTITY de SQL. Para tomar ventaja, abra cada una de las tablas de su
aplicacin y cree un campo ndice primario si no tiene uno (las tablas hijas son buenas
candidatas). MODIFY STRUCTURE, agregue los nombres de los PKField (puede usar
PKFIELD para cada tabla si lo desea), entonces use las siguientes instrucciones para
agregar los ndice nicos:

REPLACE ALL "pkfield" WITH RECNO()

or

REPLACE ALL "pkfield" WITH TRANSFORM(RECNO(),"@L #######" ) para


campos de caracteres.
Cuando termine, realice un SELECT MAX(NombreCampo) FROM (NombreTabla)
para cada una de su DBFs, y asegrese que stos son que los valores que aparecen en la
tabla llamada Keys que podr encontrar en el fichero zip de este proyecto. Esta tabla
contiene el ltimo valor del ndice primario usado en cada una de las tablas de la la
aplicacin. La misma existe tanto para sistemas basados en DBFs o para SQL. Es una
terminacin floja, y una aplicacin comercial que use esta tcnica necesitar contar con
una prueba slida despus de migrar los datos antes de comenzar a ejecutar la aplicacin
nuevamente. Tendr que hacerlo manualmente, o escribir el procedimiento de
comprobacin como un ejercicio. (La indirecta: necesitar una lista con los nombres de
las tablas y sus campos primarios para automatizar el proceso.)

Creando la base de datos SQL y cargando nuestras tablas

Ya estamos listos para construir la base de datos de SQL y cargar los datos. La manera
fcil es abrir el Enterprise Manager y crear los archivos MDF y LDF manualmente,
estimando el tamao inicial de cada uno. O podemos usar simplemente el comando
CREATE DATABASE y dejar que el SQL les asigne un tamao predefinido de medio
megabyte a cada fichero. Podemos especificar dnde poner estos archivos, pero es
preferible mantener su valor predeterminado, que podra ser: Program Files\Microsoft
SQL Server\MSSQL\Data.

Le aconsejo adems crear un userid y su contrasea para que entre a la base de datos.
Nuevamente, podemos hacerlo con el Enterprise Manager o en el cdigo, pero visual es
mejor. Vamos al tabulador Security para agregar un inicio de sesin, asignando (por
ahora) todos los derechos al userid. Permita al DBAs preocuparse por la afinacin de la
seguridad. Ellos quieren dejar fuera a las personas; nosotros queremos dejarlos entrar.

Para los propsitos de este artculo, asumir que ha realizado estos dos pasos, y que el
siguiente comando pueda manejar su conexin:

Handle = SQLStringConnect ( "driver={SQL


Server};server=local);database=MyDB;pwd=ABC;uid=def;]

Handle debe ser un nmero entero positivo, ejemplo 1, 2, 3, etc. Para cerrar la conexin
debe usar SQLDISCONNECT(0). Ya estamos listos. El siguiente programa le
preguntar donde estn sus DBFs y las carga a la base de datos nombrada anteriormente
en el servidor de SQL:

Listing 1....: LoadSQLTables.PRG

* Purpose....: Crea un duplicado de cada una de las DBF ubicada en el


directorio de datos

* a su servidor SQL y copia todos los artculos del DBF


en la tabla SQL.

* El programa pone entre parntesis los nombres de


palabras reservadas. Si se genera

* un error significa un uso ilegal de una palabra


reservada, agregula aqu:

SET TALK OFF

CLEAR

CLOSE ALL

SET STRICTDATE TO 0

SET SAFETY OFF

SET EXCLUSIVE ON

SET CONFIRM ON

ConnStr = [Driver={SQL
Server};Server=(local);UID=sa;PWD=;Database=NorthWind;]

Handle = SQLSTRINGCONNECT( ConnStr )

IF Handle < 1
MESSAGEBOX( "Imposible conectar con SQL" + CHR(13) + ConnStr, 16 )

RETURN

ENDIF

ReservedWords =
[,DESC,DATE,RESERVED,PRINT,ID,VIEW,BY,DEFAULT,CURRENT,KEY,ORDER,CHECK,
FROM,TO,]

DataPath = GETDIR("Donde se encuentran sus DBFs?")

set step on

IF LASTKEY() = 27 && Tecla ESC fue presionada

RETURN

ENDIF

IF NOT EMPTY ( DataPath )

SET PATH TO &DataPath

ENDIF

ADIR( laDBFS, ( DataPath + [*.DBF] ) )

ASORT(laDBFS,1)

* Cargar cada tabla de SQL

FOR I = 1 TO ALEN(laDBFS,1)

USE ( laDBFS(I,1))

_VFP.Caption = "Loading " + ALIAS()

LoadOneTable()

ENDFOR

SQLDISCONNECT(0)

_VFP.Caption = [Done]

PROCEDURE LoadOneTable

LOCAL I

cRecCount = TRANSFORM(RECCOUNT())
cmd = [DROP TABLE ] + ALIAS()

SQLEXEC( Handle, Cmd )

* Saltar estas tablas

IF ALIAS() $ [COREMETA/DBCXREG/SDTMETA/SDTUSER/FOXUSER/] && saltar


las tablas del sistema,

&& agregue
las suyas aqu.

? [Skipping ] + ALIAS()

RETURN

ENDIF

CreateTable() && vea debajo

SCAN

WAIT WINDOW [Loading record ] + TRANSFORM(RECNO()) + [/] +


cRecCount NOWAIT

Cmd = [INSERT INTO ] + ALIAS() + [ VALUES ( ]

FOR I = 1 TO FCOUNT()

fld = FIELD(I)

IF TYPE(Fld) = [G]

LOOP

ENDIF

dta = &Fld

typ = VARTYPE(dta)

cdta = ALLTRIM(TRANSFORM(dta))

cdta = CHRTRAN ( cdta, CHR(39),CHR(146) ) && remove any


single quotes

DO CASE

CASE Typ $ [CM]

Cmd = Cmd + ['] + cDta + ['] + [, ]

CASE Typ $ [IN]

Cmd = Cmd + cDta + [, ]

CASE Typ = [D]


IF cDta = [/ /]

cDta = []

ENDIF

Cmd = Cmd + ['] + cDta + ['] + [, ]

CASE Typ = [T]

IF cDta = [/ /]

cDta = []

ENDIF

Cmd = Cmd + ['] + cDta + ['] + [, ]

CASE Typ = [L]

Cmd = Cmd + IIF('F'$cdta,[0],[1]) + [, ]

ENDCASE

ENDFOR

Cmd = LEFT(Cmd,LEN(cmd)-2) + [ )]

lr = SQLEXEC( Handle, Cmd )

IF lr < 0

? [Error: ] + Cmd

SUSPEND

ENDIF

ENDSCAN

WAIT CLEAR

PROCEDURE CreateTable

LOCAL J

Cmd = [CREATE TABLE ] + ALIAS() + [ ( ]

AFIELDS(laFlds)

FOR J = 1 TO ALEN(laFlds,1)

IF laFlds(J,2) = [G]

LOOP

ENDIF
FldName = laFlds(J,1)

IF [,] + FldName + [,] $ ReservedWords

FldName = "[" + FldName + "]"

ENDIF

Cmd = Cmd + FldName + [ ]

DO CASE

CASE laFlds(J,2) = [C]

Cmd = Cmd + [Char(] + TRANSFORM(laFlds(J,3)) + [) NOT


NULL DEFAULT '', ]

CASE laFlds(J,2) = [I]

Cmd = Cmd + [Integer NOT NULL DEFAULT 0, ]


CASE laFlds(J,2) = [M]
Cmd = Cmd + [Text NOT NULL DEFAULT '', ]
CASE laFlds(J,2) = [N]
N = TRANSFORM(laFlds(J,3))
D = TRANSFORM(laFlds(J,4))
Cmd = Cmd + [Numeric(] + N + [,] + D + [) NOT NULL
DEFAULT 0, ]
CASE laFlds(J,2) $ [TD]
Cmd = Cmd + [SmallDateTime NOT NULL DEFAULT '', ]
CASE laFlds(J,2) = [L]
Cmd = Cmd + [Bit NOT NULL DEFAULT 0, ]
ENDCASE
ENDFOR
Cmd = LEFT(Cmd,LEN(cmd)-2) + [ )]
lr = SQLEXEC( Handle, Cmd )
IF lr < 0
_ClipText = Cmd
? [Couldnt create table ] + ALIAS()
MESSAGEBOX( Cmd )
SUSPEND
ENDIF
? [Created ] + ALIAS()
ENDPROC
Para cada DBF que no se encuentre en "Saltar estas tablas" vamos al procedimiento
LoadOneTable, el programa emite un DROP TABLE "Nombre", seguido de un
CREATE TABLE para construirla. Recorre todos los artculos en el DBF, crea y ejecuta
una declaracin de INSERT para cada uno. Los usuarios se asombran a menudo de la
rpidez con que transcurre el proceso de cargar los datos. Yo no. Es FoxPro.

Escribiendo la aplicacin de ejemplo

Ahora, estamos listos para escribir nuestra aplicacin de ejemplo. Quiero poder cambiar
a voluntad entre DBFs y SQL, para poder verificar que mi programa trabaja de la misma
manera con ambos bancos de datos. Por lo tanto mi men (he agregado llamadas a un
par de pantallas que construiremos en este artculo) quedar de la siguiente forma:

Listing 2....Menu.MPR
SET SYSMENU TO

SET SYSMENU AUTOMATIC

DEFINE PAD FilePad OF _MSYSMENU PROMPT "File"

DEFINE PAD TablePad OF _MSYSMENU PROMPT "Tables"

DEFINE PAD DataPad OF _MSYSMENU PROMPT "Change data access"

ON PAD FilePad OF _MSYSMENU ACTIVATE POPUP file

ON PAD TablePad OF _MSYSMENU ACTIVATE POPUP tables

ON SELECTION PAD DataPad OF _MSYSMENU DO ChangeAccess IN MENU.MPR

DEFINE POPUP file MARGIN RELATIVE

DEFINE BAR 1 OF file PROMPT "E\<xit"

ON SELECTION BAR 1 OF file CLEAR EVENTS

DEFINE POPUP tables MARGIN RELATIVE SHADOW COLOR SCHEME 4

DEFINE BAR 1 OF tables PROMPT "Customers" SKIP FOR WEXIST


( "frmCustomer" )

DEFINE BAR 2 OF tables PROMPT "Employees" SKIP FOR WEXIST([Employees])

ON SELECTION BAR 1 OF tables do form frmCustomer

ON SELECTION BAR 2 OF tables do form Employee

PROCEDURE ChangeAccess

oDataTier.AccessMethod = UPPER(INPUTBOX("Data access method",


"DBF/SQL/XML/WC", "DBF" ))

Qu es oDataTier? Es lo siguiente. El control oDataTier controla todo el acceso a los


datos. Por lo que primero haremos ser agregar una propiedad llamada AccessMethod, y
usar el mtodo de asignacin para capturar y validar la misma, entonces todo lo que
necesitemos ser basado en el mtodo que hayamos seleccionado. Ms sobre el tema
posteriormente.

Mi MAIN.PRG fue creado con unas pocas funciones, poner un ttulo en la pantalla,
instanciar mi objeto DataTier, posicionar el men e iniciar el ciclo de eventos:
Listing 3....: MAIN.PRG
* Purpose.....: Programa PRINCIPAL para la aplicacin

CLEAR ALL
CLOSE ALL
CLEAR
CLOSE ALL
SET TALK OFF
SET CONFIRM ON
SET MULTILOCKS ON
SET CENTURY ON
SET EXCLUSIVE ON
SET SAFETY OFF
SET DELETED ON
SET STRICTDATE TO 0

WITH _Screen
.AddObject ( [Title1], [Title], 0, 0 )
.AddObject ( [Title2], [Title], 3, 3 )
.Title2.ForeColor = RGB ( 255, 0, 0 )
ENDWITH

ON ERROR DO ErrTrap WITH LINENO(), PROGRAM(), MESSAGE(), MESSAGE(1)

DO MENU.MPR

oDataTier = NEWOBJECT ( [DataTier], [DataTier.PRG] )


oDataTier.AccessMethod = [DBF] && Requerido para ejecutar el mtodo
de Assignacin

IF NOT EMPTY ( oDataTier.AccessMethod )


READ EVENTS
ENDIF

ON ERROR

SET PROCEDURE TO
SET CLASSLIB TO

SET SYSMENU TO DEFAULT

WITH _Screen
.RemoveObject ( [Title1] )
.RemoveObject ( [Title2] )
ENDWITH

DEFINE CLASS Title AS Label


Visible = .T.
BackStyle = 0
FontName = [Times New Roman]
FontSize = 48
Height = 100
Width = 800
Left = 25
Caption = [My application]
ForeColor = RGB ( 192, 192, 192 )

PROCEDURE Init
LPARAMETERS nTop, nLeft
THIS.Top = _Screen.Height - 100 - nTop
THIS.Left= 25 - nLeft
ENDPROC
ENDDEFINE

PROCEDURE ErrTrap
LPARAMETERS nLine, cProg, cMessage, cMessage1
OnError = ON("Error")
ON ERROR
IF NOT FILE ( [ERRORS.DBF] )
CREATE TABLE ERRORS ( ;
Date Date, ;
Time Char(5), ;
LineNum Integer, ;
ProgName Char(30), ;
Msg Char(240), ;
CodeLine Char(240) )
ENDIF
IF NOT USED ( [Errors] )
USE ERRORS IN 0
ENDIF
SELECT Errors
INSERT INTO Errors VALUES ( DATE(), LEFT(TIME(),5), nLine, cProg,
cMessage, cMessage1 )
USE IN Errors

cStr = [Error at line ] + TRANSFORM(nLine) + [ of ] + cprog + [:] +


CHR(13) ;
+ cMessage + CHR(13) + [Code
that caused the error:] + CHR(13)
+ cMessage1 Figura 1: Pantalla principal de la aplicacin
IF MESSAGEBOX( cStr, 292,
[Continue] ) <> 6
SET SYSMENU TO DEFAULT
IF TYPE ( [_Screen.Title1] )
<> [U]
_Screen.RemoveObject
( [Title2] )
_Screen.RemoveObject
( [Title1] )
ENDIF
CLOSE ALL
RELEASE ALL
CANCEL
ELSE
ON ERROR &OnError
ENDIF
Fig. 1 muestra la pantalla principal de la aplicacin. La tercera seleccin del men debe
parecer interesante.

Al final del artculo, podr hacer clic en l y probar los formularios que usan distintas
fuentes de datos. El cdigo fuente esta listo para descargar en la ltima pgina. Podrn
descargar este o cualquier otro cdigo fuente desde mi sitio. Vea
http://www.lespinter.com/ para ms detalles.

La plantilla del formulario

El formulario base para esta aplicacin es un formulario plano. La mayora, si no todas,


las aplicaciones tienen uno o ms de stos, y algunas tienen docenas. As que es
inmediatamente til, mantener la simplicidad suficiente para hacer el cdigo fcil de
entender. Una vez que cosechemos xitos en los comienzos, plantillas ms complejas y
mtodos de acceso sern ms fciles de construir.

Lo que resulta un poco complicado es entender como los formularios plantilla y la capa
de datos interactan recprocamente. Durante la depresin, mi padre tena una manera
humorstica para ablandar la situacin difcil, era decir a alguien "si tuviramos un poco
de jamn, podramos tener jamn y huevos, si tuviramos algunos huevos... ". La
plantilla del formulario y la capa de datos se escriben cada una con la otra en la mente.
Los componentes reusables entran la capa de datos, mientras las partes procesales "paso
a paso" entran la plantilla.

Por ejemplo, he aqu el cdigo de la clase para mi plantilla FlatFileForm. Lo comentar


ms adelante.

Listing 4....: The FlatFileForm class

MainTable = .F. && Nombre de la tabla principal del formulario

KeyField = .F. && Nombre del ndice primario de la tabla


principal

KeyValue = .F. && Valor del ndice del campo del record actual

Beforeadd = .F. && Nmero del record antes de adicionar (usado


cuando Add es cancelado)

SearchForm = .F. && Nombre del formulario "seleccione uno"


llamado por el botn Find

Adding = .F. && Verdadero si "Save" si se hace clic sobre el


botn "Add"

Inputfields = "MYTEXT,MYCHECK,MYEDIT,MYCOMBO,MYSPIN,MYDATE" && Clases


para habilitar/deshabilitar

Beforeadd = 0 && Valor del puntero del record antes de


comenzar a editar o agregar

PROCEDURE Buttons && Habilita los botones del formulario cuando


sea necesario (on/off)

LPARAMETERS OnOff

WITH THISFORM

.cmdAdd.Enabled = OnOff

.cmdFind.Enabled = OnOff

.cmdClose.Enabled = OnOff

.cmdEdit.Enabled = OnOff AND RECCOUNT() > 0

.cmdDelete.Enabled = OnOff AND RECCOUNT() > 0


.cmdSave.Enabled = NOT OnOff

.cmdCancel.Enabled = NOT OnOff

.cmdClose.Cancel = OnOff

.cmdCancel.Cancel = NOT OnOff

ENDWITH

ENDPROC

PROCEDURE Inputs && Habilita/Deshabilita los controles del


formulario

LPARAMETERS OnOff

WITH THISFORM

FOR EACH Ctrl IN .Controls

IF UPPER ( Ctrl.Class ) $ UPPER ( .InputFields )

Ctrl.Enabled = OnOff

ENDIF

ENDFOR

.Buttons ( NOT OnOff )

ENDWITH

ENDPROC

PROCEDURE Load && Se ejecuta cuando el formulario es


instanciado

WITH THISFORM

IF EMPTY ( .MainTable )

MESSAGEBOX( [No ha especificado tabla primaria], 16, [Programmer


error], 2000 )

RETURN .F.

ENDIF

oDataTier.CreateCursor ( .MainTable, .Keyfield )

ENDWITH

ENDPROC
PROCEDURE Init && Se ejecuta despus que los botones han sido
inicializados
THISFORM.Buttons ( .T. )
ENDPROC

PROCEDURE Unload && Cierra la tabla o el cursor abierto por este


formulario
WITH THISFORM
IF USED ( .MainTable )
USE IN ( .MainTable )
ENDIF
ENDWITH
ENDPROC

PROCEDURE cmdAdd.Click && Adiciona un nuevo artculo,


autopropagando el campo ndice
WITH THISFORM
cNextKey = oDataTier.GetNextKeyValue ( .MainTable )
SELECT ( .MainTable )
.BeforeAdd = RECNO()
CURSORSETPROP( [Buffering], 3 )
APPEND BLANK
IF TYPE ( .KeyField ) <> [C]
cNextKey = VAL ( cNextKey )
ENDIF
REPLACE ( .Keyfield ) WITH cNextKey
.Refresh
.Inputs ( .T. )
.Adding = .T.
ENDWITH
ENDPROC

PROCEDURE cmdEdit.Click && Inicia la edicin del artculo actual


WITH THISFORM
SELECT ( .MainTable )
.BeforeAdd = RECNO()
CURSORSETPROP( [Buffering], 3 )
.Inputs ( .T. )
.Adding = .F.
ENDWITH
ENDPROC

PROCEDURE cmdDelete.Click && Elimina el artculo actual


WITH THISFORM
IF MESSAGEBOX( [Eliminar este artculo?], 292, _VFP.Caption ) = 6
oDataTier.DeleteRecord ( .MainTable, .KeyField )
DELETE NEXT 1
GO TOP
.Refresh
ENDIF
ENDWITH
ENDPROC

PROCEDURE cmdSave.Click && Salva los datos en el cursor local y


remotamente (si no es DBF)
WITH THISFORM
SELECT ( .MainTable )
TABLEUPDATE(.T.)
CURSORSETPROP( [Buffering], 1 )
.Inputs ( .F. )
oDataTier.SaveRecord( .MainTable, .KeyField, .Adding )
ENDWITH
ENDPROC
PROCEDURE cmdCancel.Click && Cancela la edicin o la insercin
actual
WITH THISFORM
SELECT ( .MainTable )
TABLEREVERT(.T.)
CURSORSETPROP( [Buffering], 1 )
.Inputs ( .F. )
IF BETWEEN ( .BeforeAdd, 1, RECCOUNT() )
GO ( .BeforeAdd )
ENDIF
.Refresh
ENDWITH
ENDPROC

PROCEDURE cmdFind.Click && Si est usando DBF y ningn formulario de


bsqueda es definido, use BROWSE
WITH THISFORM
IF EMPTY ( .SearchForm ) AND oDataTier,AccessMethod = [DBF]
SELECT ( .MainTable )
.BeforeAdd = RECNO()
ON KEY LABEL ENTER KEYBOARD CHR(23)
ON KEY LABEL RIGHTCLICK KEYBOARD CHR(23)
BROWSE NOAPPEND NOEDIT NODELETE
ON KEY LABEL ENTER
ON KEY LABEL RIGHTCLICK
IF LASTKEY() = 27
IF BETWEEN ( .BeforeAdd, 1, RECCOUNT() )
GO ( .Beforeadd )
ENDIF
ENDIF
ELSE
DO FORM ( .SearchForm ) TO RetVal
IF NOT EMPTY ( RetVal )
oDataTier.GetOneRecord ( .MainTable, .KeyField, RetVal )
.Refresh
.Buttons ( .T. )
ENDIF
ENDIF
ENDWITH
ENDPROC

PROCEDURE cmdClose.Click && Cerrar el formulario


THISFORM.Release
ENDPROC

Como usar esta plantilla

Para usar esta plantilla, debemos realizar 8 pasos:

1. Asigne los mapeos de los campos para use la librera Pinter.VCX y los controles
apropiados (MyText, MyChk, etc) para los tipos de datos que usar en sus
tablas;
2. Ejecute CREATE FORM "NombreFormulario" AS FlatFileForm FROM Pinter;
3. Abra el formulario, agregue el DBF que ser la tabla principal del formulario al
Data Environment, arrastre los campos hacia el formulario, y elimnela
seguidamente del Data Environment.
4. Cambie el orden de tabulacin y asigne a cmdAdd y cmdEdit las primeras
posiciones en la lista de tabulacin;
5. Asigne a las propiedades MainTable y KeyField del formulario los valores
correspondientes;
6. Elimine cualquier campo que no desee que los usuarios puedan entrar o editar,
como CreateDate o RecordID.
(Si lo prefiere puede ejecutar un BROWSE sobre su .SCX, buscar el control que
desea hacer no editable, y cambiar el valor en el campo Class del .SCX a noedit,
pues el control TextBox se encuentra deshabilitado en Pinter.Vcx. Ya que no
aparece en la propiedad InputFields como campo editable nunca ser editable.

7. Si desea que el valor de KeyField aparezca en la pantalla, agregue una etiqueta


en algn lugar del formulario y adicione el siguiente cdigo al mtodo Refresh
del formulario:
8. THISFORM.txtKeyField.Caption =
TRANSFORM( EVALUATE( THISFORM.MainTable + ;
9. [.] + THISFORM.KeyField )
10. Agregue un DO FORM "nombre" a la barra del men.
Recompile y ejecute, debe funcionar. Excepto por el formulario de bsqueda,
que es lo prximo.

He seguido mis instrucciones y he construido este pequeo formulario de Cliente en


aproximadamente 90 segundos:

Figura 2: Un ejemplo de formulario Cliente basado en la clase FlatFileForm

Es una plantilla de formulario bastante simple, y podra preguntarse si realmente hace


todo lo que necesita. Lo hace si usted incluye la capa de datos y el formulario de
bsqueda.

Una plantilla para el formulario de bsqueda


No inclu ninguna navegacin en la plantilla de FlatFileForm porque los botones
Next/Previous/First/Last son un artefacto del mundo del xBASE. Podramos
proporcionar en una sola lnea de cdigo esas cuatro facilidades (no contando las
condiciones de comprobacin BOF() / EOF() ), Pero los usuarios no ponen inters sobre
el record anterior o el prximo. Ellos desean ver la lista de candidatos, apuntar sobre el
que quieren, y clic.

He incluido una clase llamada EasySearch, originalmente publicada en el boletn de


VFUG. La misma permite agregar un formulario de bsqueda de hasta 4 campos (y es
fcil extender eso a 8 o 10 de ser necesario), permite a los usuarios filtrar los artculos,
seleccionar uno, y devolver el valor del ndice, ya sea desde un DBF, SQL o un Web
Service, con absolutamente ninguna codificacin en el propio formulario.

Simplemente rellene tres o cuatro propiedades, nombre los campos de entrada que puso
en el formulario de bsqueda con los nombres SEARCH1, SEARCH2, SEARCH3 y
SEARCH4, y asigne el orden de tabulacin en el formulario, y todo est hecho.

Aqu el cdigo:

Listing 5....: DEFINE CLASS EasySearch AS modalform

tablename = ([]) && Nombre de la tabla a buscar


colwidths = ([]) && Lista de anchos relativos delimitados por comas
colnames = ([]) && Lista de campos delimitados por comas
orderby = ([]) && "Ordenar por" nombre de columna
colheadings = ([]) && Lista delimitada por comas si no desea usar los
nombres de campo como ttulos
keyfield = ([]) && Nombre del campo ndice para retornar el valor

PROCEDURE Init
WITH THISFORM
.Caption = [Formulario de Bsqueda - ] + .Name + [ (Tabla Principal: ]
;
+ TRIM(.TableName)+[) Data access: ] + .Access
NumWords = GETWORDCOUNT(.ColNames,[,])
IF NumWords > 4
MESSAGEBOX( [Esta clase solo soporta 4 campos como mximo, lo
siento], ;
16, _VFP.Caption )
RETURN .F.
ENDIF
FOR I = 1 TO NumWords
.Field(I) = GETWORDNUM(.ColNames, I,[,])
.Heading(I) = GETWORDNUM(.ColHeadings,I,[,])
.ColWidth(I)= GETWORDNUM(.ColWidths, I,[,])
ENDFOR
WITH .Grid1
.ColumnCount = NumWords
.RecordSource = THISFORM.ViewName
.RecordSourceType = 1
GridWidth = 0
FOR I = 1 TO NumWords
.Columns(I).Header1.Caption = THISFORM.Heading (I)
GridWidth = GridWidth + VAL( THISFORM.ColWidth(I) )
FldName = THISFORM.ViewName + [.] + THISFORM.Field (I)
.Columns(I).ControlSource = FldName
ENDFOR
Multiplier = ( THIS.Width / GridWidth ) * .90 && "Fudge" factor
FOR I = 1 TO NumWords
.Columns(I).Width = VAL( THISFORM.ColWidth(I) ) * Multiplier
ENDFOR
.Refresh
ENDWITH
* Buscar cualquier control llamado SEARCHn (n = 1, 2, ... )
FOR I = 1 TO .ControlCount
Ctrl = .Controls(I)
IF UPPER(Ctrl.Name) = [MYLABEL] && Esto es, si
comienza con "MyLabel"
Sub = RIGHT(Ctrl.Name,1) && Determinar el index
IF TYPE([THISFORM.Search]+Sub)=[O] && Un campo de bsqueda
#"Sub" existe
Ctrl.Visible = .T.
Ctrl.Enabled = .T.
Ctrl.Caption = .Heading(VAL(Sub))
.SearchFieldCount = MAX ( VAL(Sub), .SearchFieldCount )
ENDIF
ENDIF
ENDFOR
.SetAll ( "Enabled", .T. )
ENDWITH
ENDPROC

PROCEDURE Load

WITH THISFORM

IF EMPTY ( .TableName )

MESSAGEBOX( [Table name not entered], 16, _VFP.Caption )

RETURN .F.

ENDIF

IF EMPTY ( .ColNames )

Msg = [ColNames property not filled in.]

MESSAGEBOX( Msg, 16, _VFP.Caption )

RETURN .F.

ENDIF

IF EMPTY ( .ColWidths )

.ColWidths = [1,1,1,1,1]

ENDIF

IF EMPTY ( .ColHeadings )

.ColHeadings = .ColNames

ENDIF

.Access = oDataTier.AccessMethod
.ViewName = [View] + .TableName

oDataTier.CreateView ( .TableName )

ENDWITH

ENDPROC

PROCEDURE Unload

WITH THISFORM

IF USED ( .ViewName )

USE IN ( .ViewName )

ENDIF

RETURN .ReturnValue

ENDWITH

ENDPROC

PROCEDURE cmdShowMatches.Click

WITH THISFORM

Fuzzy = IIF ( THISFORM.Fuzzy.Value = .T., [%], [] )

STORE [] TO Expr1,Expr2,Expr3,Expr4

FOR I = 1 TO .SearchFieldCount

Fld = [THISFORM.Search] + TRANSFORM(I) + [.Value]

IF NOT EMPTY ( &Fld )

LDD = IIF ( VARTYPE( &Fld) = [D], ;

IIF ( .Access = [DBF],[{],['] ), ;

IIF(VARTYPE( &Fld) = [C], ['],[]) )

RDD = IIF ( VARTYPE( &Fld) = [D], ;

IIF ( .Access = [DBF],[}],['] ), ;

IIF(VARTYPE( &Fld) = [C], ['],[]) )

Cmp = IIF ( VARTYPE( &Fld) = [C], [ LIKE ],[ = ] )

Pfx = IIF ( VARTYPE( &Fld) = [C], Fuzzy, [] )

Sfx = IIF ( VARTYPE( &Fld) = [C], [%], [] )


Exp = [Expr] + TRANSFORM(I)

&Exp = [ AND UPPER(] + .Field(I) + [)] + Cmp ;

+ LDD + Pfx + UPPER(ALLTRIM(TRANSFORM(EVALUATE(Fld)))) +


Sfx + RDD

ENDIF

ENDFOR

lcExpr = Expr1 + Expr2 + Expr3 + Expr4

IF NOT EMPTY ( lcExpr )

lcExpr = [ WHERE ] + SUBSTR ( lcExpr, 6 )

ENDIF

lcOrder = IIF(EMPTY(.OrderBy),[],[ ORDER BY ] ;

+ ALLTRIM(STRTRAN(.OrderBy,[ORDER BY],[])))

Cmd = [SELECT * FROM ] + .TableName + lcExpr + lcOrder


oDataTier.SelectCmdToSQLResult ( Cmd )
SELECT ( .ViewName )
ZAP
APPEND FROM DBF([SQLResult])
GO TOP
.Grid1.Refresh
IF RECCOUNT() > 0
.cmdSelect.Enabled = .T.
.Grid1.Visible = .T.
.Grid1.Column1.Alignment = 0
.Caption = [Search Form - ] + PROPER(.Name) ;
+ [ (] + TRANSFORM(RECCOUNT()) + [ matches)]
ELSE
.Caption = [Search Form - ] + PROPER(.Name)
MESSAGEBOX( "No records matched" )
.cmdSelect.Enabled = .F.
ENDIF
KEYBOARD [{BackTab}{BackTab}{BackTab}{BackTab}{BackTab}]
ENDWITH
ENDPROC

PROCEDURE cmdClear.Click
WITH THISFORM
FOR I = 1 TO .SearchFieldCount
Fld = [THISFORM.Search] + TRANSFORM(I) + [.Value]
IF VARTYPE ( &Fld ) <> [U]
lVal = IIF ( VARTYPE( &Fld) = [C], [], ;
IIF ( VARTYPE( &Fld) = [D], {//}, ;
IIF ( VARTYPE( &Fld) = [L], .F., ;
IIF ( VARTYPE( &Fld) $ [IN], 0, [?]))))
&Fld = lVal
ENDIF
ENDFOR
ENDWITH
ENDPROC

PROCEDURE cmdSelect.Click
WITH THISFORM
lcStrValue = TRANSFORM(EVALUATE(.KeyField))
.ReturnValue = lcStrValue
.Release
ENDWITH
ENDPROC

PROCEDURE cmdCancel.Click
WITH THISFORM
.ReturnValue = []
.Release
ENDWITH
ENDPROC
ENDDEFINE

Como usar esta plantilla

Aqu un ejemplo de como usar esta plantilla en tan solo 5 pasos:

1. Ejecute CREATE FORM FindCust AS EasySearch FROM Pinter


2. Adicione 2 textbox y un combobox, llame a estos tres controles Search1,
Search2 y Search3.

3. Asigne el orden de tabulacin al formulario de bsqueda


4. Cambie el valor de las propiedades MainTable a Customers, KeyField a
CustomerID y ColumnList a [CompanyName, ContactName, Phone]
5. Asigne "FindCust" al valor de la propiedad SearchForm en su formulario
Customers. No es tan malo para no escribir cdigo, eh?

Echemos una mirada a nuestro formulario FindCust:


Figura 3: El formulario FindCust

La capa de datos

An tenemos un eslabn - la biblioteca de clases DataTier (Capa de Datos) tema de este


artculo. Despus de observar todos los lugares desde donde llamamos a nuestros
mtodos, seremos capaces de ajustar el cdigo precedente con los mtodos listados ms
adelante y ver como trabajan mutuamente.

La capa de datos es usada al instanciar el objeto oDataTier en MAIN.PRG. Despus de


eso, cada vez que necesitemos enviar datos a nuestra fuente de datos, llamaremos a
nuestro objeto oDataTier.

Es importante enfatizar que en esta metodologa, de cualquier modo abriremos un DBF


o un CURSOR por cada tabla. Si agregamos o editamos, ponemos el buffering a 3, si
cambiamos algo, entonces llamamos a TableUpdate() y el buffering vuelve a 1. (Si el
usuario cancela, solo refrescamos la pantalla y todo lo que estaba antes - - el original,
reaparece el registro inalterado. Tablerevert() logra que, con tan solo agregando un poco
de cdigo regresar al registro donde nos encontrabmos antes de ejecutar el APPEND
BLANK). Donde se use Foxpro, siempre se sentir en casa.

Sin embargo, si est usando SQL o un XML Web Service, est solo a mitad de camino.
Hasta el momento, solo hemos hecho el equivalente a salvar los cambios a un Dataset
en .NET. Ahora necesita usar los datos guardados para actualizarlos, los cuales podran
estar en el Servidor del SQL o en Timbuktu. Por lo que an necesitamos TableUpdate(),
TableRevert() y las llamadas a los botones Save y Cancel. Apenas tenemos que agregar
una llamada ms al objeto DataTier y ver qu falta. Si usa DBFs, como usted ver,
simplemente disclpese y retorne. Similarmente, en el evento Load del formulario,
llamamos al mtodo CreateCursor del objeto, en ambos casos crea un cursor, o, en el
caso de acceso a un DBF, abre la tabla apropiada y su archivo ndice.

La manera ptima de leer este cdigo es encontrar el lugar en la plantilla del formulario
donde es llamada cada rutina de cdigo, entonces podremos ver los procedimientos que
DataTier ejecuta seguidamente despus que son invocados en la plantilla. Es
precisamente esta combinacin del cdigo que radica en la plantilla y la capa de datos
donde aparece la magia. He aqu el cdigo de la capa de datos: Cualquier esfuerzo por
asignar un valor a esta propiedad se entrampar por el mtodo del "setter"
AccessMethod_Assign.

Listing 6....: DataTier.PRG

DEFINE CLASS DataTier AS Custom

AccessMethod = [] && Cualquier asignacin quedar atrapada por


AccessMethod_Assign.

ConnectionString = [Driver={SQL
Server};Server=(local);Database=Northwind;UID=sa;PWD=;]

Handle = 0

Apuesto que no conoca que poda escribir sus propios mtodos Assign ...

PROCEDURE AccessMethod_Assign

PARAMETERS AM

DO CASE

CASE AM = [DBF]

THIS.AccessMethod = [DBF] && Tablas de FoxPro

CASE AM = [SQL]

THIS.AccessMethod = [SQL] && Servidor MS Sql

THIS.GetHandle

CASE AM = [XML]

THIS.AccessMethod = [XML] && FoxPro XMLAdapter

CASE AM = [WC]

THIS.AccessMethod = [WC] && Servidor WebConnection

OTHERWISE

MESSAGEBOX( [Mtodo de acceso incorrecto ] + AM, 16, [Setter


error] )

THIS.AccessMethod = []

ENDCASE
_VFP.Caption = [Data access method: ] + THIS.AccessMethod

ENDPROC

CreateCursor abre el DBF si AccessMethod realmente es DBF; por otra parte usa una
estructura devuelta de la fuente de datos para crear un cursor que se vincula a los
controles de la pantalla:

PROCEDURE CreateCursor
LPARAMETERS pTable, pKeyField
IF THIS.AccessMethod = [DBF]
IF NOT USED ( pTable )
SELECT 0
USE ( pTable ) ALIAS ( pTable )
ENDIF
SELECT ( pTable )
IF NOT EMPTY ( pKeyField )
SET ORDER TO TAG ( pKeyField )
ENDIF
RETURN
ENDIF
Cmd = [SELECT * FROM ] + pTable + [ WHERE 1=2]
DO CASE
CASE THIS.AccessMethod = [SQL]
SQLEXEC( THIS.Handle, Cmd )
AFIELDS ( laFlds )
USE
CREATE CURSOR ( pTable ) FROM ARRAY laFlds
CASE THIS.AccessMethod = [XML]
CASE THIS.AccessMethod = [WC]
ENDCASE

Este es el mtodo Assign que llamamos anteriormente en la propiedad AccessMethod:

PROCEDURE GetHandle
IF THIS.AccessMethod = [SQL]
IF THIS.Handle > 0
RETURN
ENDIF
THIS.Handle = SQLSTRINGCONNECT( THIS.ConnectionString )
IF THIS.Handle < 1
MESSAGEBOX( [Unable to connect], 16, [SQL Connection error],
2000 )
ENDIF
ELSE
Msg = [A SQL connection was requested, but access method is ] +
THIS.AccessMethod
MESSAGEBOX( Msg, 16, [SQL Connection error], 2000 )
THIS.AccessMethod = []
ENDIF
RETURN

PROCEDURE GetMatchingRecords
LPARAMETERS pTable, pFields, pExpr
pFields = IIF ( EMPTY ( pFields ), [*], pFields )
pExpr = IIF ( EMPTY ( pExpr ), [], ;
[ WHERE ] + STRTRAN ( UPPER ( ALLTRIM ( pExpr ) ), [WHERE ],
[] ) )
cExpr = [SELECT ] + pFields + [ FROM ] + pTable + pExpr
IF NOT USED ( pTable )
RetVal = THIS.CreateCursor ( pTable )
ENDIF
DO CASE
CASE THIS.AccessMethod = [DBF]
&cExpr
CASE THIS.AccessMethod = [SQL]
THIS.GetHandle()
IF THIS.Handle < 1
RETURN
ENDIF
lr = SQLExec ( THIS.Handle, cExpr )
IF lr >= 0
THIS.FillCursor()
ELSE
Msg = [Unable to return records] + CHR(13) + cExpr
MESSAGEBOX( Msg, 16, [SQL error] )
ENDIF
ENDCASE
ENDPROC

En la plantilla EasySearch, abro un nuevo cursor, el nombre de que le doy "View"


seguido por el nombre de la tabla que estamos cargando. Por lo que no es una vista, solo
un nombre para el cursor:

PROCEDURE CreateView
LPARAMETERS pTable
IF NOT USED( pTable )
MESSAGEBOX( [Can't find cursor ] + pTable, 16, [Error creating
view], 2000 )
RETURN
ENDIF
SELECT ( pTable )
AFIELDS( laFlds )
SELECT 0
CREATE CURSOR ( [View] + pTable ) FROM ARRAY laFlds
ENDFUNC

GetOneRecord es llamado para cualquier tipo de fuente de datos. Si usamos un DBF,


con un comando LOCATE podemos obtener el artculo a travs del ndice, esta
bsqueda es optimizada por Rushmore y es muy rpida. De la misma forma SQL
tambin utiliza la optimizacin Rushmore, en SQL 2000 est implcita - sin ninguna
mencin de donde viene. Pero ya sabemos eso ...

PROCEDURE GetOneRecord
LPARAMETERS pTable, pKeyField, pKeyValue
SELECT ( pTable )
Dlm = IIF ( TYPE ( pKeyField ) = [C], ['], [] )
IF THIS.AccessMethod = [DBF]
cExpr = [LOCATE FOR ] + pKeyField + [=] + Dlm + TRANSFORM
( pKeyValue ) + Dlm
ELSE
cExpr = [SELECT * FROM ] + pTable + [ WHERE ] + pKeyField + [=] + ;
Dlm + TRANSFORM ( pKeyValue ) + Dlm
ENDIF
DO CASE
CASE THIS.AccessMethod = [DBF]
&cExpr
CASE THIS.AccessMethod = [SQL]
lr = SQLExec ( THIS.Handle, cExpr )
IF lr >= 0
THIS.FillCursor( pTable )
ELSE
Msg = [Unable to return record] + CHR(13) + cExpr
MESSAGEBOX( Msg, 16, [SQL error] )
ENDIF
CASE THIS.AccessMethod = [XML]
CASE THIS.AccessMethod = [WC]
ENDCASE
ENDFUNC

FillCursor es anlogo al mtodo Fill del DataAdapters de .NET. El comando ZAP de


FoxPro es el mtodo DataAdapter.Clear en .NET. Aadiendo desde DBF(CursorName)
en el cursor nombrado, no rompemos el vnculo con los controles del formulario:

PROCEDURE FillCursor
LPARAMETERS pTable
IF THIS.AccessMethod = [DBF]
RETURN
ENDIF
SELECT ( pTable )
ZAP
APPEND FROM DBF ( [SQLResult] )
USE IN SQLResult
GO TOP
ENDPROC

Quisiera pensar que todos los ndices primarios sern enteros, pero los sistemas del
antiguos a veces tienen ndices de carcter. Esta es la razn por la que chequeo los
separadores en la rutina DeleteRecord:

PROCEDURE DeleteRecord
LPARAMETERS pTable, pKeyField
IF THIS.AccessMethod = [DBF]
RETURN
ENDIF
KeyValue = EVALUATE ( pTable + [.] + pKeyField )
Dlm = IIF ( TYPE ( pKeyField ) = [C], ['], [] )
DO CASE
CASE THIS.AccessMethod = [SQL]
cExpr = [DELETE ] + pTable + [ WHERE ] + pKeyField + [=] + ;
Dlm + TRANSFORM ( m.KeyValue ) + Dlm
lr = SQLExec ( THIS.Handle, cExpr )
IF lr < 0
Msg = [Unable to delete record] + CHR(13) + cExpr
MESSAGEBOX( Msg, 16, [SQL error] )
ENDIF
CASE THIS.AccessMethod = [XML]
CASE THIS.AccessMethod = [WC]
ENDCASE
ENDFUNC

La rutina SaveRecord realiza un INSERT o un UPDATE en depencia si el usuario est


agregando o no:

PROCEDURE SaveRecord
PARAMETERS pTable, pKeyField, pAdding
IF THIS.AccessMethod = [DBF]
RETURN
ENDIF
IF pAdding
THIS.InsertRecord ( pTable, pKeyField )
ELSE
THIS.UpdateRecord ( pTable, pKeyField )
ENDIF
ENDPROC

Las rutinas InsertRecord y UpdateRecord llaman a funciones diferentes, las cuales


construyen rdenes INSERT o UPDATE de SQL, de la misma manera que hace .NET.
Guardo la cadena resultante en _ClipText para poder chequearla con el Query Analyzer
y ver manualmente si algo sali mal. Es la manera ms rpida de poner a punto nuestro
cdigo de SQL generado. Finalmente, uso SQLExec() para ejecutar la instruccin.
SQLExec() devuelve -1 si hay un problema.

PROCEDURE InsertRecord
LPARAMETERS pTable, pKeyField
cExpr = THIS.BuildInsertCommand ( pTable, pKeyField )
_ClipText = cExpr
DO CASE
CASE THIS.AccessMethod = [SQL]
lr = SQLExec ( THIS.Handle, cExpr )
IF lr < 0
msg = [Unable to insert record; command follows:] + CHR(13)
+ cExpr
MESSAGEBOX( Msg, 16, [SQL error] )
ENDIF
CASE THIS.AccessMethod = [XML]
CASE THIS.AccessMethod = [WC]
ENDCASE
ENDFUNC

PROCEDURE UpdateRecord
LPARAMETERS pTable, pKeyField
cExpr = THIS.BuildUpdateCommand ( pTable, pKeyField )
_ClipText = cExpr
DO CASE
CASE THIS.AccessMethod = [SQL]
lr = SQLExec ( THIS.Handle, cExpr )
IF lr < 0
msg = [Unable to update record; command follows:] + CHR(13)
+ cExpr
MESSAGEBOX( Msg, 16, [SQL error] )
ENDIF
CASE THIS.AccessMethod = [XML]
CASE THIS.AccessMethod = [WC]
ENDCASE
ENDFUNC

FUNCTION BuildInsertCommand
PARAMETERS pTable, pKeyField
Cmd = [INSERT ] + pTable + [ ( ]
FOR I = 1 TO FCOUNT()
Fld = UPPER(FIELD(I))
IF TYPE ( Fld ) = [G]
LOOP
ENDIF
Cmd = Cmd + Fld + [, ]
ENDFOR
Cmd = LEFT(Cmd,LEN(Cmd)-2) + [ } VALUES ( ]
FOR I = 1 TO FCOUNT()
Fld = FIELD(I)
IF TYPE ( Fld ) = [G]
LOOP
ENDIF
Dta = ALLTRIM(TRANSFORM ( &Fld ))
Dta = CHRTRAN ( Dta, CHR(39), CHR(146) ) && librse de comillas
en los datos
Dta = IIF ( Dta = [/ /], [], Dta )
Dta = IIF ( Dta = [.F.], [0], Dta )
Dta = IIF ( Dta = [.T.], [1], Dta )
Dlm = IIF ( TYPE ( Fld ) $ [CM],['],;
IIF ( TYPE ( Fld ) $ [DT],['],;
IIF ( TYPE ( Fld ) $ [IN],[], [])))
Cmd = Cmd + Dlm + Dta + Dlm + [, ]
ENDFOR
Cmd = LEFT ( Cmd, LEN(Cmd) -2) + [ )] && Remove ", " add " )"
RETURN Cmd
ENDFUNC

FUNCTION BuildUpdateCommand
PARAMETERS pTable, pKeyField
Cmd = [UPDATE ] + pTable + [ SET ]
FOR I = 1 TO FCOUNT()
Fld = UPPER(FIELD(I))
IF Fld = UPPER(pKeyField)
LOOP
ENDIF
IF TYPE ( Fld ) = [G]
LOOP
ENDIF
Dta = ALLTRIM(TRANSFORM ( &Fld ))
IF Dta = [.NULL.]
DO CASE
CASE TYPE ( Fld ) $ [CMDT]
Dta = []
CASE TYPE ( Fld ) $ [INL]
Dta = [0]
ENDCASE
ENDIF
Dta = CHRTRAN ( Dta, CHR(39), CHR(146) ) && librse de
comillas en los datos
Dta = IIF ( Dta = [/ /], [], Dta )
Dta = IIF ( Dta = [.F.], [0], Dta )
Dta = IIF ( Dta = [.T.], [1], Dta )
Dlm = IIF ( TYPE ( Fld ) $ [CM],['],;
IIF ( TYPE ( Fld ) $ [DT],['],;
IIF ( TYPE ( Fld ) $ [IN],[], [])))
Cmd = Cmd + Fld + [=] + Dlm + Dta + Dlm + [, ]
ENDFOR
Dlm = IIF ( TYPE ( pKeyField ) = [C], ['], [] )
Cmd = LEFT ( Cmd, LEN(Cmd) -2 ) ;
+ [ WHERE ] + pKeyField + [=] ;
+ + Dlm + TRANSFORM(EVALUATE(pKeyField)) + Dlm
RETURN Cmd
ENDFUNC

En ocasiones, necesito devolver un cursor que usar para otros propsitos, por ejemplo,
llenar un combobox. Utilizo el nombre predeterminado SQLResult. Menciono este
detalle porque si utilizo el SELECT de Foxpro devuelve el cursor por defecto en un
BROWSE, y necesito la certeza que el nombre del cursor ser SQLResult cuando
retorne de este procedimiento:

PROCEDURE SelectCmdToSQLResult
LPARAMETERS pExpr
DO CASE
CASE THIS.AccessMethod = [DBF]
pExpr = pExpr + [ INTO CURSOR SQLResult]
&pExpr
CASE THIS.AccessMethod = [SQL]
THIS.GetHandle()
IF THIS.Handle < 1
RETURN
ENDIF
lr = SQLExec ( THIS.Handle, pExpr )
IF lr < 0
Msg = [Unable to return records] + CHR(13) + cExpr
MESSAGEBOX( Msg, 16, [SQL error] )
ENDIF
CASE THIS.AccessMethod = [XML]
CASE THIS.AccessMethod = [WC]
ENDCASE
ENDFUNC

Cuando agrego un nuevo registro, sea DBFS o SQL, soy total responsable al insertar un
nico valor en la tabla. Mantengo una tabla con nombres de las tablas y el ltimo ndice
usado. Si crea esta tabla manualmente, asegrese de tener actualizado el LastKeyVal
manualmente, o conseguir miles de llaves duplicadas.<G>

FUNCTION GetNextKeyValue
LPARAMETERS pTable
EXTERNAL ARRAY laVal
pTable = UPPER ( pTable )
DO CASE
CASE THIS.AccessMethod = [DBF]
IF NOT FILE ( [Keys.DBF] )
CREATE TABLE Keys ( TableName Char(20), LastKeyVal
Integer )
ENDIF
IF NOT USED ( [Keys] )
USE Keys IN 0
ENDIF
SELECT Keys
LOCATE FOR TableName = pTable
IF NOT FOUND()
INSERT INTO Keys VALUES ( pTable, 0 )
ENDIF
Cmd = [UPDATE Keys SET LastKeyVal=LastKeyVal + 1 ] ;
+ [ WHERE TableName='] + pTable + [']
&Cmd
Cmd = [SELECT LastKeyVal FROM Keys WHERE TableName = '] ;
+ pTable + [' INTO ARRAY laVal]
&Cmd
USE IN Keys
RETURN TRANSFORM(laVal(1))

CASE THIS.AccessMethod = [SQL]


Cmd = [SELECT Name FROM SysObjects WHERE Name='KEYS' AND
Type='U']
lr = SQLEXEC( THIS.Handle, Cmd )
IF lr < 0
MESSAGEBOX( "SQL Error:"+ CHR(13) + Cmd, 16 )
ENDIF
IF RECCOUNT([SQLResult]) = 0
Cmd = [CREATE TABLE Keys ( TableName Char(20), LastKeyVal
Integer )]
SQLEXEC( THIS.Handle, Cmd )
ENDIF
Cmd = [SELECT LastKeyVal FROM Keys WHERE TableName='] + pTable
+ [']
lr = SQLEXEC( THIS.Handle, Cmd )
IF lr < 0
MESSAGEBOX( "SQL Error:"+ CHR(13) + Cmd, 16 )
ENDIF
IF RECCOUNT([SQLResult]) = 0
Cmd = [INSERT INTO Keys VALUES ('] + pTable + [', 0 )]
lr = SQLEXEC( THIS.Handle, Cmd )
IF lr < 0
MESSAGEBOX( "SQL Error:"+ CHR(13) + Cmd, 16 )
ENDIF
ENDIF
Cmd = [UPDATE Keys SET LastKeyVal=LastKeyVal + 1 WHERE
TableName='] + pTable + [']
lr = SQLEXEC( THIS.Handle, Cmd )
IF lr < 0
MESSAGEBOX( "SQL Error:"+ CHR(13) + Cmd, 16 )
ENDIF
Cmd = [SELECT LastKeyVal FROM Keys WHERE TableName='] +
pTable + [']
lr = SQLEXEC( THIS.Handle, Cmd )
IF lr < 0
MESSAGEBOX( "SQL Error:"+ CHR(13) + Cmd, 16 )
ENDIF
nLastKeyVal = TRANSFORM(SQLResult.LastKeyVal)
USE IN SQLResult
RETURN TRANSFORM(nLastKeyVal)

CASE THIS.AccessMethod = [WC]


CASE THIS.AccessMethod = [XML]

ENDCASE

ENDDEFINE

Ejecutando la aplicacin

Teclee BUILD EXE ThreeTier FROM ThreeTier, o pulse el botn Build en el Project
Manager. Concludo el proceso, ejectelo y pruebe cada formulario, y las pantallas de
bsqueda. Funciona bastante bien, tal como hasta ahora hemos visto con los DBFs.

Probemos entonces de una forma diferente, seleccione Change Data Source desde el
men y seleccione SQL, como se muestra en la Fig. 4.
Figura 4: Cambiando el mtodo de acceso a los datos

En este instante, abra el formulario Customers. No existen datos! Eso es normal para
una aplicacin de SQL, porque no se han realizado peticiones de registros por el
usuario, todava no se han hecho. Haga clic en Find, teclee algn criterio de bsqueda
(realmente, para devolcer todos los registros djelos en blanco) haga clic sobre Show
Matches, y seleccione uno. El formulario de bsqueda desaparecer, y tendr sus
datos!. Seleccione Edit, cambie algo y slvelo. Cargue nuevamente la pgina para
verificar que todo funciona. Agregue un artculo, slvelo, y compruebe que aparece en
el formulario de bsqueda.

Nada especial ha sucedido. Excepto que ha construido su primera aplicacin SQL


usando Foxpro, sin escribir una lnea de cdigo en ninguno de los formularios

Proximamente adicionaremos soporte para XML Web Services y para el tan venerable
WebConnection.

Nos vemos.

Cdigo Fuente

Les Pinter es miembro del INETA Speaker's Bureau y director de Pinter Consulting en
San Mateo, California. Publica boletines bimestrales para desarrolladores con
artculos sobre Visual Foxpro, VB.NET, ASP.NET y SQL. Con frecuencia es orador en
grupos de usuarios y conferencias en Estados Unidos y el extranjero, dialogando en
Ingls, Portugus, Espaol, Ruso y Francs. Les tambin es piloto privado.