28 de abril de 2006

Funciones de fecha del API de Windows en Visual FoxPro

Estas funciones hacen más fácil trabajar con los nombres de los días y los nombres del mes en el idioma/formato configurado por el usuario en el sistema operativo , utilizando las funciones API disponibles en Windows.

CDOWEX(dFecha, nFormato)
Acepta como primer parámetro una fecha o un número de día del 1 al 7 (1 es Lunes). Devuelve una cadena con el nombre completo o el nombre abreviado correspondiente al día de la semana.
  • dFecha: es una expresión de fecha, o un número del 1 al 7 que representa un día de la semana, siendo 1 el día Lunes.
  • nFormato: indica el formato deseado, puede ser 1, "S" (Short), "C" (Corto) ó 2, "L" (Long o Largo). El valor por omisión es 2.
Ejemplo:
? CDOWEX(1,2)
Lunes
? CDOWEX(Date(2007,1,3),1)
Mié
CMONTHEX(dFecha, nFormato)
Acepta como primer parámetro una fecha o un número de mes del 1 al 12. Devuelve una cadena con el nombre completo o abreviado correspondiente al mes.
  • dFecha: es una expresión de fecha, o un número del 1 al 12 que representa un mes del año, siendo 1 el mes Enero.
  • nFormato: indica el formato deseado, puede ser 1, "S" (Short), "C" (Corto) ó 2, "L" (Long o Largo). El valor por omisión es 2.
Ejemplo:
? CMONTHEX(1,1), CMONTHEX(1,2)
Ene Enero
? CMONTHEX(Date(2007,12,7),1), CMONTHEX(12,2)
Dic Diciembre
CDATEEX(dFecha, nFormato)
Acepta como primer parámetro una fecha o una expresión cualquiera. Devuelve una cadena con la fecha formateada según el formato del sistema.
  • dFecha: es una expresión de fecha, si se pasa cualquier otro tipo de valor, la función utiliza el valor de DATE() como expresión de fecha.
  • nFormato: indica el formato deseado, puede ser 1, "S" (Short), "C" (Corto) ó 2, "L" (Long o Largo). El valor por omisión es 2.
Ejemplo:
? CDATEEX(Date(),1)
20/04/2006
? CDATEEX(Date(),2)
Jueves, 20 de Abril de 2006
Otro ejemplo:
? Transform(Day(Date())) + [/] + CMONTHEX(Date(),1) + ; 
  [/] + Transform((Year(Date())))
20/Abr/2006

NOTA: Dado que los valores devueltos por estas funciones dependen de la configuración del formato de fecha y del idioma del sistema operativo Windows, en cada caso los valores pueden ser distintos a los mostrados en los ejemplos, y pueden variar en cada equipo de acuerdo al gusto del usuario.

CODIGO:

20 de abril de 2006

Accesos directos a distintas versiones de VFP


Llevo mas de una década como desarrollador de aplicaciones en Visual FoxPro y tengo en mis carpetas de desarrollo varios proyectos con aplicaciones en distintas versiones del producto. Los nuevos desarrollos con la última versión, otros en proceso de migración y otros que seguramene quedarán en versiones anteriores por diversos motivos. Muchas veces deseo elegir con que versión puedo abrir, desarrollar o compilar estos proyectos. ¿Quién no, verdad?.

Imagino que la mayoría de los desarrolladores inician la versión elegida de Visual FoxPro desde el menú Inicio de Windows y ejecutan algo así como:
SET DEFAULT TO C:\Desarrollo\AplicacionX
para asi comenzar y abrir el proyecto ubicado en la carpeta AplicacionX

Personalmente utilizo desde hace muchos años un truco que me enseñó mi "maestro" José Marcenaro (ex MVP de VFP) que realmente es muy práctico e inicia la versión elegida de Visual FoxPro directamente en la carpeta de desarrollo del proyecto, con solo hacer doble clic en un archivo.

Estos simples archivos, que podemos crear vacios o con algún caracter, pueden tener cualquier nombre, pero una extensión que identifica la versión de VFP a iniciar. Por ejemplo el archivo "iniciar.vf8" iniciará Visual FoxPro 8.

En cada carpeta de mis proyectos copio los distintos tipos de archivos que me iniciaran las distintas versiones de VFP. En mi caso, y como se muestra en la siguiente imagen, utilizo el nombre de archivo "__vfp__" para que estos queden en primer lugar en la lista ordenada por nombres ascendentemente.


Para registrar todas estas extensiones e indicar la aplicación asociada y su ícono, solo se debe copiar lo siguiente en un archivo de texto que guardaremos con la extensión ".reg" y lo ejecutaremos por única vez haciendo doble clic sobre el mismo.

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.vf6]
@="Visual.FoxPro.Shortcut.6"
[HKEY_CLASSES_ROOT\.vf7]
@="Visual.FoxPro.Shortcut.7"
[HKEY_CLASSES_ROOT\.vf8]
@="Visual.FoxPro.Shortcut.8"
[HKEY_CLASSES_ROOT\.vf9]
@="Visual.FoxPro.Shortcut.9"
[HKEY_CLASSES_ROOT\.vfp]
@="Visual.FoxPro.Shortcut.9"

[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.6]
@="Acceso directo a Visual FoxPro 6"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.6\DefaultIcon]
@="%SystemRoot%\\system32\\Shell32.dll,84"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.6\shell]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.6\shell\open]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.6\shell\open\command]
@="C:\\Archivos de programa\\Microsoft Visual Studio\\vfp98\\vfp6.exe"

[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.7]
@="Acceso directo a Visual FoxPro 7"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.7\DefaultIcon]
@="%SystemRoot%\\system32\\Shell32.dll,84"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.7\shell]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.7\shell\open]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.7\shell\open\command]
@="C:\\Archivos de programa\\Microsoft Visual FoxPro 7\\vfp7.exe"

[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.8]
@="Acceso directo a Visual FoxPro 8"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.8\DefaultIcon]
@="%SystemRoot%\\system32\\Shell32.dll,84"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.8\shell]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.8\shell\open]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.8\shell\open\command]
@="C:\\Archivos de programa\\Microsoft Visual FoxPro 8\\vfp8.exe"

[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.9]
@="Acceso directo a Visual FoxPro 9"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.9\DefaultIcon]
@="%SystemRoot%\\system32\\Shell32.dll,84"
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.9\shell]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.9\shell\open]
[HKEY_CLASSES_ROOT\Visual.FoxPro.Shortcut.9\shell\open\command]
@="C:\\Archivos de programa\\Microsoft Visual FoxPro 9\\vfp9.exe"

En este caso se registraran las extensiones .vp6, .vp7, .vf8, .vf9 y también la extensión .vfp usada para la última versión de Visual FoxPro, o la versión que nosotros mas utilicemos.
Importante: Advertir que en el contenido del archivo .reg mostrado arriba, se toman las rutas de instalación por omisión de las distintas versiones de Visual FoxPro en un sistema operativo en español. Si sus rutas son distintas, debe indicarlo en el archivo .reg en las líneas correspondientes.
Espero que encuentren de utilidad esta nueva forma de iniciar nuestro querido ZORRO.

Hasta la próxima.

Luis María Guayán

11 de abril de 2006

Las vistas remotas pueden compartir una conexion establecida por SPT

Las funciones SQLCONNECT() y SQLSTRINGCONNECT() fueron mejoradas a partir de VFP 8 para aceptar un parámetro opcional lShared.

Esta mejora permite abrir vistas remotas en un manejador (handle) establecido por SQLCONNECT() o SQLSTRINGCONNECT(), que no era posible en versiones anteriores de VFP.

Use la nueva cláusula CONNSTRING [handle] para abrir una vista remota usando una conexión SPT existente:

LOCAL lnHandle
lnHandle = SQLSTRINGCONNECT("DRIVER=sql server;SERVER=(local);UID=sa;PWD=;DATABASE=Northwind",.t.)
USE MiVistaRemota IN 0 NODATA CONNSTRING lnHandle
o
LOCAL lnHandle
lnHandle = SQLCONNECT("DSNName",.t.)
USE MiVistaRemota IN 0 NODATA CONNSTRING lnHandle

Note que este mejora también cambia el valor retornado por CURSORGETPROP('ConnectHandle'..), a partir de VFP 8, como se demuestra en el código del siguiente ejemplo.
*
*  Ejemplo de compartir una conexión SPT con una vista remota
*    SQLCONNECT()/SQLSTRINGCONNECT() parámetro lShared
*    USE MiVistaRemota CONNSTRING nHandle
*
CLOSE ALL
CLEAR
ERASE SPT.D*
CREATE DATABASE SPT
*
? "Creando la conexión C_Test en SPT.DBC..."
CREATE CONNECTION C_Test CONNSTRING ;
  "DRIVER=sql server;SERVER=(local);UID=sa;PWD=;DATABASE=Northwind"
DBSETPROP('C_Test', 'Connection', 'Asynchronous', .F.)
DBSETPROP('C_Test', 'Connection', 'BatchMode', .T.)
DBSETPROP('C_Test', 'Connection', 'Comment', '')
DBSETPROP('C_Test', 'Connection', 'DispLogin', 3)
DBSETPROP('C_Test', 'Connection', 'ConnectTimeOut', 15)
DBSETPROP('C_Test', 'Connection', 'DispWarnings', .F.)
DBSETPROP('C_Test', 'Connection', 'IdleTimeOut', 0)
DBSETPROP('C_Test', 'Connection', 'QueryTimeOut', 0)
DBSETPROP('C_Test', 'Connection', 'Transactions', 1)
*
?  "Creando una vista remota en SPT.DBC..."
CREATE SQL VIEW RV_Customers REMOTE CONNECTION C_Test SHARE AS ;
  SELECT * FROM Customers
CREATE SQL VIEW RV_Orders REMOTE CONNECTION C_Test SHARE AS ;
  SELECT * FROM Orders
CREATE SQL VIEW RV_Products REMOTE CONNECTION C_Test SHARE AS ;
  SELECT * FROM Customers
SQLDISCONNECT(0)
#IF VERSION(5) >= 800
  **************  SQLCONNECT()
  *!*  LOCAL lnUnSharedHandle, lnSharedHandle
  *!*  lnUnSharedHandle = SQLCONNECT("DSNName","sa","",.t.)
  *!*  ? "lnUnSharedHandle = ", TRANSFORM(lnUnSharedHandle)
  *!*  *  EL cuarto parámetro es nuevo y especifica
  *!*  *  que la conexión puede ser compartida
  *!*  *  con vistas remotas
  *!*  lnSharedHandle = SQLCONNECT("DSNName","sa","",.t.)
  *!*  ? "lnSharedHandle = ", TRANSFORM(lnSharedHandle)
  **************  SQLSTRINGCONNECT()
  LOCAL lnUnSharedHandle, lnSharedHandle
  lnUnSharedHandle = SQLSTRINGCONNECT("DRIVER=sql server;SERVER=(local);UID=sa;PWD=;DATABASE=Northwind")
  ? "lnUnSharedHandle = ", TRANSFORM(lnUnSharedHandle)
  *  El segundo parámetro es nuevo y especifica
  *  que la conexión puede ser compartida con 
  *  vistas remotas. Esto no era posible con
  *  versiones anteriores de VFP
  lnSharedHandle = SQLSTRINGCONNECT("DRIVER=sql server;SERVER=(local);UID=sa;PWD=;DATABASE=Northwind",.T.)
  ? "lnSharedHandle = ", TRANSFORM(lnSharedHandle)
  *
  *  El lnHandle especificadoen la cláusula CONNSTRING
  *  es nuevo e indica la existencia de un manejador
  *  por el cual se abrirá la vista. No tiene que estar
  *  definida como SHARE
  *
  DisplayProperties("UnSharedConnection",lnUnSharedHandle)
  DisplayProperties("SharedConnection",lnSharedHandle)
  ?
  ? "USE these views using the new CONNSTRING lnSharedHandle construct:"
  USE SPT!RV_Customers IN 0 NODATA CONNSTRING lnSharedHandle
  DisplayProperties("RV_Customers",CURSORGETPROP("ConnectHandle","RV_Customers"))
  USE SPT!RV_Orders IN 0 NODATA CONNSTRING lnSharedHandle
  DisplayProperties("RV_Orders",CURSORGETPROP("ConnectHandle","RV_Orders"))
  USE SPT!RV_Products IN 0 NODATA CONNSTRING lnSharedHandle
  DisplayProperties("RV_Products",CURSORGETPROP("ConnectHandle","RV_Products"))
  USE IN RV_Customers
  USE IN RV_Orders
  USE IN RV_Products
  SQLDISCONNECT(0)
#ENDIF
*
*  El siguiente comportamiento en VFP8 es distinto que en todas las versiones previas de VFP.
*
*  Antes de VFP8, las vistas remotas, eran creadas con la palabra SHARE
*  si tuvieran el mismo CURSORGETPROP("ConnectHandle"). En VFP8 cada una tiene un 
*  diferente CURSORGETPROP("ConnectHandle"), pero todas se abren por la 
*  misma conexión ODBC
*
?
? "USE these SHAREd views without any CONNSTRING clause -- "
? "this behavior is different starting in VFP 8, where "
? '    CURSORGETPROP("ConnectHandle",..)'
? "returns a different connection handle for each view (but the same statement handle)"
USE SPT!RV_Customers IN 0 NODATA
DisplayProperties("RV_Customers",CURSORGETPROP("ConnectHandle","RV_Customers"))
USE SPT!RV_Orders IN 0 NODATA
DisplayProperties("RV_Orders",CURSORGETPROP("ConnectHandle","RV_Orders"))
USE SPT!RV_Products IN 0 NODATA
DisplayProperties("RV_Products",CURSORGETPROP("ConnectHandle","RV_Products"))
USE IN RV_Customers
USE IN RV_Orders
USE IN RV_Products
SQLDISCONNECT(0)
CLOSE DATABASES ALL
ERASE SPT.D*
RETURN
*
PROCEDURE DisplayProperties(tcString,tnHandle)
  ? PADL(tcString,20)
  ?? "  VFP Conn Handle=" + TRANSFORM(tnHandle)
  ?? "  ODBC Hdbc=" + TRANSFORM(SQLGETPROP(tnHandle,"ODBChdbc") )
  ?? "  ODBC Hstmt=" + TRANSFORM(SQLGETPROP(tnHandle,"ODBChstmt"))
ENDPROC
VFP Tips & Tricks - Drew Speedie

6 de abril de 2006

Mover un formulario sin la barra de título

Tres opciones de como mover un formulario sin la barra de título, tomandolo con el puntero del ratón desde cualquier zona libre del formulario. Los siguientes ejemplos fueron enviadas al Grupo de Noticias de Microsoft Visual FoxPro en Español. Se destacan con otro color las diferencias entre ellos.

1. Opción enviada por Hugo M. Ranea
PUBLIC goForm
goForm = CREATEOBJECT("MiForm")
goForm.SHOW(1)
RETURN

DEFINE CLASS MiForm AS FORM
  AUTOCENTER = .T.
  TITLEBAR = 0
  ADD OBJECT cmdSalir AS COMMANDBUTTON WITH ;
    TOP = 10, LEFT = 10, CAPTION = "Salir", ;
    HEIGHT = 25, NAME = "cmdSalir"
   FUNCTION LOAD
    DECLARE INTEGER ReleaseCapture IN WIN32API
    DECLARE INTEGER SendMessage IN WIN32API ;
      INTEGER, INTEGER, INTEGER, INTEGER
  ENDFUNC
  FUNCTION MOUSEDOWN
    LPARAMETERS nButton, nShift, nXCoord, nYCoord
    LOCAL lnHandle
    IF nButton = 1
      ReleaseCapture()
      SendMessage(THISFORM.HWND, 0x112, 0xF012,0)
    ENDIF
  ENDFUNC
  PROCEDURE cmdSalir.CLICK
    THISFORM.RELEASE
  ENDPROC
ENDDEFINE

2. Opción enviada por Fernando D. Bozzo
PUBLIC goForm
goForm = CREATEOBJECT("MiForm")
goForm.SHOW(1)
RETURN

DEFINE CLASS MiForm AS FORM
  AUTOCENTER = .T.
  TITLEBAR = 0
  ADD OBJECT cmdSalir AS COMMANDBUTTON WITH ;
    TOP = 10, LEFT = 10, CAPTION = "Salir", ;
    HEIGHT = 25, NAME = "cmdSalir"
  PROCEDURE MOUSEMOVE
    LPARAMETERS nButton, nShift, nXCoord, nYCoord
    DO WHILE MDOWN()
      THISFORM.MOVE(THISFORM.LEFT + MCOL(THISFORM.NAME,3) - nXCoord, ;
        THISFORM.TOP + MROW(THISFORM.NAME,3) - nYCoord)
    ENDDO
  ENDPROC
  PROCEDURE cmdSalir.CLICK
    THISFORM.RELEASE
  ENDPROC
ENDDEFINE

3. Opción enviada por Luis María Guayán
PUBLIC goForm
goForm = CREATEOBJECT("MiForm")
goForm.SHOW(1)
RETURN

DEFINE CLASS MiForm AS FORM
  AUTOCENTER = .T.
  TITLEBAR = 0
  ADD OBJECT cmdSalir AS COMMANDBUTTON WITH ;
    TOP = 10, LEFT = 10, CAPTION = "Salir", ;
    HEIGHT = 25, NAME = "cmdSalir"
  PROCEDURE LOAD
    DECLARE LONG SendMessage IN "User32";
      LONG HANDLE, LONG wMsg, ;
      LONG wParam, LONG LPARAM
  ENDPROC
  PROCEDURE MOUSEMOVE
    LPARAMETERS nButton, nShift, nXCoord, nYCoord
    SendMessage(THISFORM.HWND, 0x202, 0, 0)
    SendMessage(THISFORM.HWND, 0x112, 0xF012, 0)
  ENDPROC
  PROCEDURE cmdSalir.CLICK
    THISFORM.RELEASE
  ENDPROC
ENDDEFINE

5 de abril de 2006

Auditando eventos de inserción, modificación y eliminación en tablas de FoxPro

Título original: Auditing FoxPro Table Add, Edit, and Delete Events
Autor: Nancy Folsom
Traducido por: Luis María Guayán


Hace casi un año escribí un artículo que mostró cómo crear y utilizar una herramienta sencilla para registrar los cambios de estructura a las vistas locales y a las tablas. Este mes escribo sobre una herramienta sencilla que audite los cambios del usuario (inserciones, actualizaciones y eliminaciones) a las tablas de FoxPro usando desencadenantes (triggers) de la tabla. Antes de zambullirse en el ejemplo, vale la pena la observación de que, en mi caso, no utilizo desencadenantes de relaciones de integridad (RI) de FoxPro. Si usted lo hace, entonces necesitará considerar cómo envolvería el código automáticamente generado por RI dentro del código de auditoria. También confío en el hecho de que mis tablas auditadas están en un contenedor de base de datos.

Mantengo un sistema que ahora requiere un seguimiento a los cambios a una tabla. Puesto que el sistema utiliza las tablas contenidas en un DBC, los desencadenantes y los procedimientos almacenados son herramientas naturales (los procedimientos almacenados son sólo código de programas almacenados en el contenedor de base de datos). Otro buen lugar para colocar la implementación de la auditoria, es a través de la capa de datos. No elegí esta opción puesto que estos datos se pueden acceder por lo menos de dos interfaces (front ends) distintas, una Web y una aplicación de escritorio, y porque yo estoy utilizando procedimientos almacenados en vez de servicios Web.

Si usted no ha utilizado desencadenantes de tabla antes, estos son los eventos que se pueden disparar para las tablas contenidas en una DBC siempre que ocurra un INSERT (o APPEND), un UPDATE (o TableUpdate), o DELETE. Hay algunas limitaciones de lo que usted puede hacer en los desencadenantes. Por ejemplo, no intente cambiar ningún campo en la tabla que ha generado el desencadenante. Observe también que las tablas libres no pueden tener desencadenantes, así que usted tendría que utilizar sus clases de datos (o de negocio) para auditar.

Aunque mi cliente requiere solamente la auditoria para una tabla (por lo menos hoy), he pensado con frecuencia acerca de escribir (algún día) una rutina de auditoria que pueda habilitar y deshabilitar si deseo hacer un seguimiento de uso o un cierto problema. Ésta era la oportunidad perfecta de escribirla. Usando desencadenantes de DBC, puedo habilitar y deshabilitar la auditoria usando los comandos CREATE TRIGGER y DELETE TRIGGER.

Este artículo viene con el código de los programas AuditTableStoredProc.PRG que es el código debe agregar a los procedimientos almacenados de cualquier DBC. El otro programa, AuditTrailExample.PRG que instala un ejemplo, así usted puede ver cómo éste trabaja. Para utilizarlos, cree los archivos PRGs (con los nombres mencionados) en cualquier carpeta vacía y ejecute:
DO AuditTrailExample 
desde la ventana de comandos.

AuditTrailExample.PRG
CLOSE DATABASES ALL
* Creo la tabla libre que auditará las acciones
* de inserción, modificación y eliminación
CreateAuditStorage()
* Creo algunos datos de ejemplo
CreateExampleData()
* Ahora que configuramos, ponemos algunos datos.
INSERT INTO GuineaPigTbl (;
  cSomeField) VALUES ( "Dark chocolate" )
INSERT INTO GuineaPigTbl (;
  cSomeField) VALUES ( "Milk chocolate" )
INSERT INTO GuineaPigTbl (;
  cSomeField) VALUES ( "White chocolate" )
INSERT INTO GuineaPigTbl (;
  cSomeField) VALUES ( "Truffles" )
INSERT INTO GuineaPigTbl (;
  cSomeField) VALUES ( "Hot chocolate" )
GO 2
REPLACE cSomeField WITH "Chocolate milk"
GO BOTTOM
DELETE
DELETE TRIGGER ON GuineaPigTbl FOR DELETE
USE IN GuineaPigTbl
USE TableAudit
BROWSE NOWAIT
RETURN .T.

**************************************************
* Function CreateAuditStorage()
* Este es un PRG de instalación como propósito de
* este artículo. Esto crea la tabla que almacenará
* el seguimiento de la auditoria
**************************************************
FUNCTION CreateAuditStorage()
  *
  * Elimino los datos de ejemplos anteriores
  *
  IF FILE(FULLPATH('TableAudit.DBF'))
    DELETE FILE FULLPATH('TableAudit.DBF') Recycle
  ENDIF
  *
  * Creo una tabla libre para almacenar
  * los datos auditados...
  *
  CREATE TABLE TableAudit FREE (;
    AuditId I NOT NULL AUTOINC NEXTVALUE 115 STEP 1, ;
    Entity C(64) NOT NULL, ;
    Action C(16) NOT NULL, ;
    SOURCE M NOT NULL, ;
    OldValues M NOT NULL, ;
    NewValues M NOT NULL, ;
    cUser C(64) NOT NULL, ;
    Pk C(16) NOT NULL, ;
    PkValue C(16) NOT NULL, ;
    TIMESTAMP T NOT NULL)
  *
  * Creo los índices...
  *
  INDEX ON AuditId TAG AuditId CANDIDATE
  *...
ENDFUNC

**************************************************
* Function CreateExampleData()
* Este es un PRG de instalación como propósito de
* este artículo. Esto crea una base de datos y
* tabla de ejemplo
**************************************************
FUNCTION CreateExampleData()
  * Elimino los datos de ejemplos anteriores
  IF FILE(FULLPATH('GuineaPigDBC.DBC'))
    DELETE FILE FULLPATH('GuineaPigDBC.DBC') Recycle
  ENDIF
  IF FILE(FULLPATH('GuineaPigTbl.DBF'))
    DELETE FILE FULLPATH('GuineaPigTbl.DBF') Recycle
  ENDIF
  * Creo una DBC y tabla en la cual pueda hacer
  * inserciones, modificaciones y eliminaciones
  CREATE DATABASE GuineaPigDBC
  SET DATABASE TO GuineaPigDBC
  CREATE TABLE GuineaPigTbl ;
    (iID I AUTOINC, cSomeField C(32), PRIMARY KEY iID )
  * Creo el procedimiento almacenado en la
  * base de datos GuineaPigDBC
  IF CreateStoredProc()
    * Creo un desencadenante para la tabla GuiniaPigTbl
    * usando nuestro procedimiento almacenado
    CREATE TRIGGER ON GuineaPigTbl FOR INSERT AS ;
      AuditTable()
    CREATE TRIGGER ON GuineaPigTbl FOR DELETE AS ;
      AuditTable()
    CREATE TRIGGER ON GuineaPigTbl FOR UPDATE AS ;
      AuditTable()
  ENDIF
ENDFUNC

**************************************************
* Function CreateStoredProc()
* Este es un PRG de instalación como propósito de
* este artículo. Esto agrega el procedimiento almacenado
* de auditoria a la base de datos GuineaPigDBC.
* Puede tambien simplemente cortar y pegar el código
* desde el PRG AuditTableStoredProc.Prg que está
* incluido en este artículo.
**************************************************
FUNCTION CreateStoredProc()
  IF EMPTY(DBC())
    MESSAGEBOX("Rats! I was expecting a Dbc()!")
    RETURN .F.
  ENDIF
  APPEND PROCEDURES FROM AuditTableStoredProc.Prg
  RETURN .T.
ENDFUNC

**************************************************


AuditTableStoredProc.PRG
**************************************************
* Comienzo del código de seguimiento de la auditoria
* Seguimiento de la auditoria para uso general de
* cualquier tabla (DBC)
**************************************************
FUNCTION AuditTable()
  LOCAL ;
    lcOldSetDbcTo, lcFldState,lcAlias, ;
    lcSource, lcAction, lcUser, lcPk, ;
    lcPkValue, lcNew, lcOld
  STORE "" TO lcOld, lcNew
  *
  * La DBC de la tabla debe ser la actual para DbGetProp()
  *
  lcOldSetDbcTo = SET("Database")
  IF SetDBC() && Esto no trabaja para tablas libres
    *
    * Inicializo variables que dependen de la
    * tabla seleccionada con ALIAS()
    *
    lcFldState = NVL( GETFLDSTATE(-1), "" )
    lcUser = SYS(0) && Quien lo hace...
    lcAction = GetAction( lcFldState ) && Que hace...
    lcAlias = ALIAS() && A quien...
    lcSource = GetCursorSource()
    *
    * Almaceno la clave primaria y el valor
    *
    lcPk = DBGETPROP( lcAlias, "Table", "PrimaryKey" )
    lcPkValue = FieldNameToStringValue( lcPk )
    * Colecciono los valores de los campos que han cambiado
    * GetValuesAsString( lcFldState, @lcOld, @lcNew )
    ValuesToString(lcFldState, @lcOld, @lcNew)
    *
    * Creo el registro de auditoria
    *
    INSERT INTO TableAudit (;
      Entity, Action, NewValues, OldValues, cUser, ;
      Pk, PkValue, SOURCE, TIMESTAMP ) VALUES ( ;
      lcAlias, lcAction, lcNew, lcOld, lcUser, ;
      lcPk, lcPkValue, lcSource, DATETIME() )
    *
    * Limpio
    *
    USE IN SELECT('TableAudit')
    SET DATABASE TO (lcOldSetDbcTo)
    SELECT SELECT(lcAlias)
  ENDIF
  RETURN .T.
ENDFUNC

**************************************************
* Function ValuesToString( tcFldState, tcOld, tcNew )
* Pasar tcOld y tcNew por referencia.
* Por cada campo en el registro, verifico si ha cambiado.
* Si cambio, concateno los valores de los campos
* a los parametros tcOld y tcNew pasados.
**************************************************
FUNCTION ValuesToString( tcFldState, tcOld, tcNew )
  LOCAL lcFldState, lvOld, lvNew, lni, lcField
  *
  * Quito la bandera de eliminación de la cadena GetFldState
  *
  lcFldState = SUBSTR( tcFldState, 2 )
  tcOld = ""
  tcNew = ""
  FOR lni = 1 TO LEN( lcFldState )
    IF INLIST( SUBSTR(lcFldState, lni, 1), "2", "4" )
      *
      * Algo ha cambiado
      *
      lcField = FIELD(lni)
      lvOld = OLDVAL( lcField )
      lvNew = EVALUATE( lcField )
      *
      * Almaceno los valores anteriores
      *
      IF !ISNULL( lvOld ) && Ignore if .null.
        tcOld = tcOld + ;
          lcField + " = " + Stringify( lvOld ) + CHR(13)
      ENDIF
      *
      * Almaceno los nuevos valores
      *
      IF !ISNULL( lvNew ) && Ignoro si es nulo.
        tcNew = tcNew + ;
          lcField + " = " + Stringify( lvNew ) + CHR(13)
      ENDIF
    ENDIF && Si el registro ha cambiado
  ENDFOR && Por cada campo en el registro
  RETURN .T.
ENDFUNC

**************************************************
* Function Stringify(tvValue)
* Tomo un tipo de valor y lo convierto a un valor
* pueda ser evaluado nuevamente a su tipo original.
**************************************************
*
* Definiciones para las partes de fechas
*
#DEFINE sYear STR( YEAR( tvValue ), 4, 0)
#DEFINE sMonth STR( MONTH( tvValue ), 2, 0)
#DEFINE sDay STR( DAY( tvValue ), 2, 0)
#DEFINE sHour STR( HOUR( tvValue ), 2, 0)
#DEFINE sMinute STR( MINUTE(tvValue ), 2, 0)
#DEFINE sSecond STR( SEC( tvValue ), 2, 0)
*
FUNCTION Stringify(tvValue)
  *
  LOCAL lcType, lcReturn
  lcType = VARTYPE(tvValue)
  DO CASE
    CASE lcType = "Y" && Currency
      lcReturn = ALLTRIM( STR( tvValue, 12, 4 ) )
    CASE lcType = "C" && Character
      lcReturn = "[" + ALLTRIM(tvValue) + "]"
    CASE lcType = "D" && Date
      lcReturn = "Date"
      * Set("Mark") no es soportado en el proveedor OLEDB
      * lcMark = Set("Mark") && Date separator
      lcMark = "/"
      lcReturn = "{^" + ;
        sYear + lcMark + ;
        sMonth + lcMark + ;
        sDay + "}"
    CASE lcType = "T" && DateTime
      * Set("Mark") no es soportado en el proveedor OLEDB
      * lcMark = Set("Mark") && Date separator
      lcMark = "/"
      lcReturn = "{^" + ;
        sYear + lcMark + ;
        sMonth + lcMark + ;
        sDay + " " + ;
        sHour + ":" + ;
        sMinute + ":" + ;
        sSecond + "}"
    OTHERWISE
      lcReturn = ALLTRIM( TRANSFORM(tvValue) )
  ENDCASE
  RETURN lcReturn
ENDFUNC

**************************************************
* Function GetAction
* ¿Se añadio el registro insertado, modificado o eliminado?
**************************************************
FUNCTION GetAction( tcFldState)
  LOCAL lcReturn
  DO CASE
    CASE EMPTY( CHRTRAN( tcFldState, "1", "" ) )
      lcReturn = ""
    CASE LEFT( tcFldState, 1 ) = "2"
      lcReturn = IIF( DELETED(), "Deleted", "Recalled" )
    CASE AT( "2", tcFldState ) > 0
      lcReturn = "Updated"
    CASE !EMPTY( CHRTRAN( tcFldState, "12", "" ) )
      lcReturn = "Inserted"
    OTHERWISE
      lcReturn = tcFldState
  ENDCASE
  RETURN lcReturn
ENDFUNC

**************************************************
* Function GetCursorSource()
* Retorna la(s) tabla(s) subyacente para ALIAS()
**************************************************
FUNCTION GetCursorSource()
  LOCAL lcReturn, lni
  lni = CURSORGETPROP("SourceType")
  DO CASE
    CASE lni = 1 && Vista Local
      lcReturn = CURSORGETPROP("Tables")
    CASE lni = 3 && Tabla VFP
      lcReturn = FULLPATH( DBF() )
    OTHERWISE
      lcReturn = ""
  ENDCASE
  RETURN lcReturn
ENDFUNC

**************************************************
* Function FieldNameToStringValue( tcField )
* Toma el nombre de campo y retorna el valor del
* campo como una cadena
**************************************************
FUNCTION FieldNameToStringValue( tcField )
  LOCAL lcField
  lcField = FIELD( tcField)
  IF EMPTY( lcField )
    RETURN ""
  ENDIF
  RETURN Stringify( EVALUATE( lcField ) )
ENDFUNC

**************************************************
* Function SetDBC()
**************************************************
FUNCTION SetDBC()
  LOCAL lcDBC
  lcDBC = CURSORGETPROP("Database")
  IF EMPTY(lcDBC)
    RETURN .F. && Esto no trabaja para tablas libres
  ENDIF
  SET DATABASE TO (lcDBC)
  RETURN .T.
ENDFUNC

**************************************************
* Final del código de seguimiento de la auditoria
**************************************************
Notará que el procedimiento almacenado del seguimiento de la auditoria incluye varias funciones. Éstas son las funciones de economía doméstica que procesan la información que va a ser auditada. No entraré en detalle para ambos códigos mostrados aquí. Todo el código está comentado, sin embargo, siéntase libre de probarlo. Si tiene cualquier pregunta, puede enviarme un correo electrónico a: nfolsomNOSPAM@NOSPAMpixeldustindustries.com

Comencé por decidir que quería una función que auditara cualquier desencadenante. Si necesito una auditoria especializada para alguna tabla o vista, cambiaré simplemente los desencadenadores para las tablas afectadas. También pretendí almacenar los viejos y nuevos valores pero solamente para los campos modificados. Los campos se guardan como expresiones que se puedan evaluar nuevamente a sus tipos de campo y valores originales.

La auditaría del usuario que está realizando los cambios es también útil. En esta implementación, obviamente sencilla, estoy utilizando SYS(0) que retorna el nombre de la máquina. Esto identifica que el cambio vino del sitio Web (donde un usuario tiene que ser registrado) o la estación de trabajo, si el cambio se realiza desde la interfaz de escritorio. Auditando la llave primaria del registro modificado, puedo encontrar la cuenta de los usuarios Web. Sin embargo, éste no es un sistema financiero que necesita un fuerte seguimiento de la actividad del usuario.

Mi siguiente paso en esto será una prueba con tablas muy grandes y con un gran seguimiento de auditoria. También agregaré una opción para que los usuarios limpien la tabla de seguimientos de auditoria periódicamente.

Por ahora, es solo esto. Espero que lo encuentre útil. Lo invito a contacterme por correo electrónico a nfolsomNOSPAM@NOSPAMpixeldustindustries.com con cualquier comentario, pregunta o crítica. Sus comentarios serán bienvenidos.

Nancy Folsom

4 de abril de 2006

OLEDB/ADO, el ODBC de VFP y los registros eliminados

El driver ODBC y el OLEDB Provider se comportan diferente en cuestión de los registros borrados, veremos la forma de trabajar en ambos...

El ODBC de VFP tiene un parámetro dentro de su cadena de conexión, la cuál permite establecer la configuración de SET DELETE para la sesión:

Driver={Microsoft Visual FoxPro Driver};SourceType=DBC;SourceDB=c:myvfpdb.dbc;Exclusive=No;NULL=NO;
Collate=Machine;BACKGROUNDFETCH=NO;DELETED=NO"
Puedes cambiar a YES o NO según sean su requerimientos, para el OLDB Provider de VFP no existe tal parámetro, por lo que tendríamos que hacerlo "manualmente":

Si se desea hacer desde VFP...
TEXT TO lcConnString NOSHOW TEXTMERGE PRETEXT 8
Provider=vfpoledb.1;
Data Source=<>;Collating Sequence=machine
ENDTEXT
*** Creamos la conexión ADO con la cadena de conexión del OLEDB Provider de VFP.
loConnection = CREATEOBJECT("ADODB.Connection")
loConnection.Open(lcConnString)

*** Creamos el comando ADO
loCommand = CREATEOBJECT("ADODB.Command")
*** Establecemos el comando a ejecutar
loCommand.CommandText = "SET DELETED ON"
*** Establecemos la conexión con la que se ejecutará el comando
loCommand.ActiveConnection = loConnection
*** Ejecutamos
loCommand.Execute()

Esto aplica para cualquier lenguaje de programación que se esté utilizando, por ejemplo, este sería el código para el Microsoft C# (uno de los lenguajes soportados por Microsoft .NET Framework):

OleDbConnection loConn = new OleDbConnection("Provider=vfpoledb.1;" + 
             @&amp;quot;Data Source=D:SamplesDatatestdata.dbc;" + 
              "Exclusive=false;Nulls=false");
try {
            loConn.Open();
            OleDbCommand locmd =loConn.CreateCommand();
            locmd.CommandText = "set deleted on";
            locmd.ExecuteNonQuery();
         }
         catch(Exception ex) 
         {
            this.lblErrorMsg.Text = ex.Message;
            return false;
         }
Espero les sea de utilidad.

Espartaco Palma Martínez