viernes, enero 31, 2014

Nueva versión v1.19.6 de FoxBin2Prg (Soporte SCCAPI)

Por: Fernando D. Bozzo

Acabo de liberar la versión v1.19.6 de mantenimiento, con los siguientes cambios:

- Agregado soporte para SourceSafe (SCCAPI - Diff y Merge)

- Arreglo regeneración de DataEnvironment en forms cuando hay clases con nombre menor a 'd'

- Reactivada recompilación por defecto para poder ver siempre los métodos, si no algunos no se ven


Link de descarga:
https://vfpx.codeplex.com/releases/view/116407


Les agradezco el feedback por correo y en los comentarios.

Saludos!

domingo, enero 26, 2014

Nueva versión v1.19.5 de FoxBin2Prg (Localización Multiidioma)

Por: Fernando D. Bozzo

Acabo de publicar la nueva versión de mantenimiento con los siguientes cambios:

- Agregado soporte de Localización Multiidioma. Se incluye archivo foxbin2prg_en.h con la traducción al inglés, que debe copiarse o renombrarse a foxbin2prg.h y recompilarse para que tenga efecto.

Cualquier otro idioma que se desee, se puede crear el archivo correspondiente .h
Si se quiere hacer llegar a todos los que descarguen el programa, por favor, enviármelo y lo agregaré a los archivos de traducción distribuibles.


Se puede descargar de VFPX:
https://vfpx.codeplex.com/wikipage?title=FoxBin2Prg_es

Saludos!

Crear un proyecto FoxPro ¿por dónde comenzar?

Por: Fernando D. Bozzo

Aunque esto es algo fácil una vez adquirida cierta experiencia, muchos se pierden y no tiene claro como comenzar u organizar un proyecto.

¿Pero crear un proyecto no es simplemente pulsar el botón new / Project y meterle archivos dentro? No, es más que eso, y a continuación vamos a ver una de las formas comunes de hacerlo de acuerdo a las Buenas Prácticas de programación.



Cosas a tener en cuenta


Hay dos modalidades de proyectos, los que hacen nuevos desde cero y los que se crean para poner un poco de orden dentro de uno o más programas existentes o que se quieren dejar de ejecutar con DO <programa> y se necesita hacer un EXE o APP o DLL.
Da igual que modalidad se use, la estructura del proyecto es muy importante, ya que en parte determina la escalabilidad y principalmente la transportabilidad del mismo, ya que un programa o sistema que solo funciona en una ubicación particular y deja de hacerlo si se cambia de directorio o unidad (C,E,G,etc) generará un grave problema de mantenimiento.



Estructura de directorios para Desarrollar


Esta es una estructura de directorios típica:

\raiz           => Dir.principal del PJX y PRG principal (ej: c:\desa\miapp)
     \datos     => Archivos DBF,FPT,CDX,DBC,DCX,DCT,SQL (y texto DB2/DC2)
     \bmps      => Archivos gráficos JPG,PNG,etc
     \forms     => Archivos SCX/SCX (y texto SCA/SC2)
     \clases    => Archivos VCX/VCT (y texto VCA/VC2)
     \include   => Archivos "include" (.H)
     \menus     => Archivos MNX/MNT
     \config    => Aquí puede ir el CONFIG.FPW 
     \prgs      => Archivos PRG
     \reports   => Archivos FRX/FRT/LBX/LBT (y texto FRA/FR2/LBA/LB2)

La raíz puede estar ubicada en cualquier directorio, y desde la misma se crean una serie de subdirectorios que contendrán los componentes agrupados por tipo. Esto permite ubicarlos más rápidamente y no tenerlos todos juntos de forma desordenada, donde buscar algo se puede complicar bastante. Además esta separación ayuda al mantenimiento separado de los componentes y a evitar ciertos errores de manipulación.

En la raíz suele haber 2 archivos principales: el proyecto (PJX) y el programa principal de arranque (MAIN.PRG, PRINCIPAL.PRG o como se llame)

El directorio datos es el directorio de trabajo del usuario, conteniendo todas las tablas, bases de datos, vistas e índices

Y el resto de directorios indica con su nombre el tipo de componente que contiene



El programa principal


En general conviene que el programa principal del proyecto sea un PRG (por ejemplo, MAIN.PRG) y que esté en el directorio raíz junto al proyecto, ya que es donde sea realiza la inicialización del sistema, donde se establece el directorio por defecto y donde se carga el primer form o menú del sistema.

En el caso de un sistema, este programa debería tener un contenido mínimo y dejar todas las implementaciones y procedimientos a los componentes y librerías, de forma que quede abierta la posibilidad de que, con una buena encapsulación funcional de componentes, se puedan reutilizar objetos en otros sistemas o para ser usados de otra forma, como procesos batch (por lotes).

El contenido mínimo de MAIN.PRG (o como lo llamen) para un sistema de mantenimiento (altas, bajas, modificaciones) con formularios de usuario podría ser así:

*-- MAIN.PRG
*-- Configuración del entorno
#INCLUDE INCLUDE\MAIN.H  && Define el archivo de inclusión principal de variables nombradas
PUBLIC glSalir
glSalir = .F.            && Se debe asignar .T. cuando se quiera salir
ON SHUTDOWN DO CerrarSistema
SET TALK OFF             && No mostrar el resultado de los comandos al ejecutarse
SET SAFETY OFF           && No mostrar avisos de confirmación al sobreescribir datos
SET DATE DMY
SET EXCLUSIVE OFF        && No abrir las tablas en Exclusiva, sino SHARED
SET DELETED ON           && No mostrar registros marcados como eliminados
SET CENTURY ON
SET HOURS TO 24
SET TABLEPROMPT OFF      && No mostrar ventana de selección de tabla si no encuentra el alias
SET PATH TO "clases;forms;menus;datos;prgs;bmps" && Rutas relativas a la raíz
_SCREEN.Caption = NOMBRE_SISTEMA
CD (JUSTPATH(SYS(16)))   && Cambiarse al directorio raíz (donde está MAIN.PRG)
DO MENUPRINCIPAL.MPR
DO FORM entrada_datos.scx && Ejecutar el form principal (si hay) o un menú, o ambos
DO WHILE NOT glSalir
   READ EVENTS           && Poner al sistema en espera de acciones del usuario
ENDDO

*-- Cerrar forms abiertos
FOR I = _SCREEN.FormCount TO 1 STEP -1
    _SCREEN.Forms(I).HIDE()
    _SCREEN.Forms(I).Release()
ENDFOR


RELEASE ALL
CLOSE ALL

IF _VFP.StartMode <= 1
  RETURN                 && En modo Desarrollo u objeto sale con RETURN
ENDIF

QUIT                     && En modo EXE o APP sale con QUIT

PROCEDURE CerrarSistema
   glSalir = .T.
   ON SHUTDOWN
   CLEAR EVENTS
ENDPROC
*-- Fin MAIN.PRG

Ahora vamos a ver las distintas secciones de este programa



El archivo de inclusión MAIN.H


Los archivos de inclusión permiten definir constantes nombradas, esto es, variables de sólo lectura que durante la compilación del programa se reemplazarán en el  sitio donde sean utilizadas.

Suele tener varios usos, por ejemplo para ejecutar un sistema en modo Depuración o en modo Producción se puede usar una constante DEBUGMODE y luego parametrizar código de programa para que dependiendo de su valor habilite o deshabilite ciertas partes. También se puede usar para la localización de la aplicación, que es poner en constantes todos los textos que muestre la aplicación para poder internacionalizar su interfaz y soportar diferentes idiomas.

Es conveniente usar un archivo de inclusión único que permita referenciar a otros archivos de inclusión si fuera necesario, cosa que en los forms, clases, programas y demás componentes siempre se referencie al principal y éste se encargue de referenciar a los demás.
Este sistema de referenciamiento de archivos include dentro del principal da mucha flexibilidad, ya que por un lado evita duplicar las constantes nombradas que ya están definidas en otros archivos, por ejemplo el foxpro.h que viene con FoxPro.

Este es un ejemplo de archivo de inclusión:

*-- MAIN.H
*-- VARIABLES NOMBRADAS Y LINKEO DE OTROS ARCHIVOS INCLUDE

#INCLUDE \include\foxpro.h
#DEFINE NOMBRE_SISTEMA   "GESTIÓN"

#DEFINE DEBUGMODE .T.

Como se observa, este archivo de inclusión referencia a otro archivo de inclusión (aquí foxpro.h, pero podría referenciar a otros más agregando sentencias #include) y además define una constante para el nombre del sistema que usé para definir el CAPTION de _SCREEN en MAIN.PRG



Configuración del PATH de la aplicación


Para que se puedan encontrar los componentes sin tener que referenciarlos con rutas absolutas, es necesario definir el Path, y lo más conveniente es hacerlo con rutas relativas al directorio raíz, tal como puse en MAIN.PRG:

SET PATH TO "clases;forms;menus;datos;prgs;bmps"

Esto indica que desde el directorio actual o raíz, busque los componentes en los subdirectorios "clases", "forms", etc.

La ventaja de definirlo así es que al mover la aplicación a otro directorio, no hará falta hacer ningún cambio en el código, ya que todo permanecerá perfectamente referenciado, sin importar si se mueve incluso a otra unidad de disco o de red, siempre que se mueva la estructura completa de directorios.



El archivo de configuración CONFIG.FPW


El CONFIG.FPW es opcional, y se usa para algunos seteos iniciales que se desea ejecutar antes de cualquier instrucción de programa y de que se muestre la pantalla principal de FoxPro. Habitualmente aquí se configuran cosas como indicar que no se desea usar el archivo de recursos (foxuser.dbf), o si se quiere que el sistema no tenga visible la ventana principal de FoxPro y solo muestre los forms del usuario o el principal, o incluso se puede configurar los directorios de temporales y de trabajo o la cantidad máxima de variables de memoria que se usarán.

Un archivo CONFIG.FPW puede contener esto:

ALLOWEXTERNAL=OFF
RESOURCE=OFF
SCREEN=OFF

Si se quiere forzar a que todos los temporales vayan a un directorio, solo hay que agregar esto (c:\temp se puede reemplazar por cualquier directorio):

EDITWORK=C:\TEMP
PROGWORK=C:\TEMP
SORTWORK=C:\TEMP
TMPFILES=C:\TEMP



Alternativamente también se puede cambiar la ubicación de todos los temporales, pero por usuario, por ejemplo:

EDITWORK=C:\APLICACION\DATOS\TEMP\USUARIO
PROGWORK=C:\APLICACION\DATOS\TEMP\USUARIO
SORTWORK=C:\APLICACION\DATOS\TEMP\USUARIO
TMPFILES=C:\APLICACION\DATOS\TEMP\USUARIO



Pero requiere algo más de trabajo, y se puede hacer con 2 archivos config. Para comenzar, se requiere un CONFIG.FPW mínimo dentro del proyecto, por ejemplo:

ALLOWEXTERNAL=ON
RESOURCE=OFF
SCREEN=OFF


Y el segundo config.fpw se puede hacer usando un programa lanzador (llamémoslo Lanzador.exe) que invoque al EXE principal del sistema (ej: sistema.exe), previa preparación del archivo de configuración.

Por ejemplo, el lanzador.exe debería hacer lo siguiente:

  1. Crear la carpeta para el usuario, según se obtenga de SYS(0)
  2. Crear un CONFIG.FPW personalizado para ese usuario (como el de arriba con EDITWORK/SORTWORK/etc, pero reemplazando USUARIO por el nombre de usuario obtenido del SYS(0)) y escribirlo en su path particular que se creó en (1)
  3. Finalmente el lanzador invoca al EXE principal con la nueva ruta:
    sistema.exe -cc:\aplicacion\datos\temp\usuario

De esta forma se pueden separar los temporales de cada usuario, lo que, cuando hay muchos usuarios, previene los errores de temporales con el mismo nombre.


En la ayuda de FoxPro, buscando por "config.fpw" en el índice, muestra todas las configuraciones.
Aunque casi todos los seteos se pueden hacer por programa (comandos SET), hay algunos que solamente se pueden hacer aquí.

Nota: El motivo de que haya puesto el CONFIG.FPW en el directorio CONFIG y no haya incluido este directorio en el Path, es por dos cosas:

  1. Porque cuando ejecuto el programa con DO MAIN.PRG en modo Desarrollo, no quiero que me oculte la pantalla o desactive el archivo de recursos
  2. Porque lo anterior quiero que lo haga solo cuando lo ejecuto como EXE.


Truco:
Para esto es fundamental incluir el archivo CONFIG.FPW en el proyecto pero no referenciar su directorio en el Path, de esta forma cuando se ejecuta el programa con DO MAIN.PRG no encontrará el CONFIG.FPW porque su directorio no está en el Path, pero cuando se ejecute como EXE, al estar incluido dentro del proyecto, el EXE encontrará todos los componentes que haya dentro del PJX, aunque no se haya configurado un SET PATH (es como un SET PATH implícito).



Capturar el cierre desde la cruz (o aspa) de la ventana


Si se desea capturar este evento para poder cerrar la ventana desde la cruz, como todas las aplicaciones Windows, entonces se puede usar la sentencia ON SHUTDOWN como hice en MAIN.PRG, que normalmente llama a un programa de cierre que configura una variable global para salir y hace un CLEAR EVENTS.

Es importante tener en cuenta que para que esto funcione no deben haber formularios modales abiertos. Solo puede haber abiertos forms normales (no modales).



Configuración del directorio principal o raíz


En el ejemplo está en MAIN.PRG y se encarga de hacer que el directorio por defecto de la aplicación sea el mismo donde está MAIN.PRG, por eso la importancia de que este programa principal esté en el directorio raíz y no dentro de PRGS con el resto de programas.

CD (JUSTPATH(SYS(16)))

La función SYS(16) devuelve el nombre del programa en ejecución con su ruta absoluta. Por ejemplo, si el programa de nuestro ejemplo está en el directorio c:\desa, SYS(16) devolverá C:\DESA\MAIN.PRG, y si fuera un ejecutable y el proyecto se llamara SISTEMA.EXE devolvería C:\DESA\SISTEMA.EXE
Con la función JUSTPATH() obtenemos la ruta del programa y finalmente con CD (Change Directory) nos cambiamos al directorio obtenido.



Ejecución de un form o menú (o ambos)


Antes de entrar al bucle READ EVENTS, se ejecuta el form o menú que se quiera mostrar al usuario. ¿Y por qué un bucle y no solo Read Events? Bueno, esto es opcional y lo ví así en algunos ejemplos de FoxPro. El posible motivo que encuentro para esto es que si se quiere controlar la salida exclusivamente con la variable pública glSalir y se tiene la costumbre de ejecutar CLEAR EVENTS en el cierre de todos los formularios, esta es un buena forma de controlar de que se vuelva a ejecutar el READ EVENTS y no se salga del sistema si no se elije expresamente salir, lo que pondrá glSalir = .T. y hará el CLEAR EVENTS definitivo.
Por qué se pueda querer hacer un CLEAR EVENTS en cada form no lo sé, pero podría haber algún caso especial que lo requiera y este método lo controla.

DO WHILE NOT glSalir     && No salir con Clear Events hasta que glSalir sea .T.
   READ EVENTS           && Poner al sistema en espera de acciones del usuario
ENDDO






Cerrar los forms abiertos


Esto es útil cuando se tiene un sistema MDI (Múltiple Document Interface) con varias ventanas que pueden estar abiertas a la vez y se quiere poder salir y cerrarlas todas sin tener que estar cerrando una a una.

*-- Cerrar forms abiertos
FOR I = _SCREEN.FormCount TO 1 STEP -1
    _SCREEN.Forms(I).HIDE()
    _SCREEN.Forms(I).Release()
ENDFOR


El motivo de que el conteo sea en orden inverso es el mismo motivo de que la eliminación de elementos de un array debe hacerse de la misma forma: Si hay 5 elementos en el array (o 5 forms en el array de forms) y se quisieran borrar en orden normal, al llegar al  4 ya no existe, porque se borraron 3, y 5 - 3 = 2, con lo cuál daría un error, y para evitar esto se hace en orden inverso.



Retorno final o cierre de sesión


Finalmente se debe devolver el control al sistema... o a la ventana de comandos. Veamos: Si estamos depurando y ejecutando con DO MAIN.PRG, no queremos que se nos cierre la sesión de FoxPro, sino que queremos permanecer en la misma para seguir trabajando, hacer pruebas en la ventana de comandos, etc., pero si estamos en el EXE y este se ejecutó con doble click en el explorador de archivos o se lanzó desde otro programa, no hay ventana de comandos a la que volver, y se debe cerrar la sesión para que no quede una "sesión colgada" donde la ventana de FoxPro no se vé pero se sigue ejecutando (se puede ver con el Administrador de Tareas)

¿Cómo distinguir si estamos ejecutando con DO MAIN.PRG o con el ejecutable? esto lo hacemos consultando la propiedad StartMode, así:

_VFP.StartMode

En la ayuda figuran todos sus valores, pero nos sirve saber que 0=Ejecución con DO, 1=Ejecución con CREATEOBJECT y el resto de valores son ejecución como EXE, DLL, etc.




Distribución de ejecutables



En el caso de que se quiera hacer un ejecutable (EXE) o APP para distribuir, y se use algún instalador, típicamente el directorio de instalación del programa será "Program Files" o "Archivos de programa", pero los datos nunca deberán estar allí, ya que desde Windows 7 ese es un directorio especialmente protegido contra escritura.

Los datos deberán ubicarse en otro directorio que no esté dentro de "Program Files" ni otro directorio del Sistema, como Windows o System32. Cualquier directorio es válido. En este caso, puede que se complique un poco indicar que los programas van en un directorio y los datos en otro que no está bajo el, y además se deberá tener en cuenta para la definición de los PATH de datos, que deberán parametrizarse.

Otra opción, que para mí es mejor, es instalar directamente el programa en otro directorio, por ejemplo, c:\app\mi_sistema, para que pueda mantener la misma estructura de directorios que en desarrollo, y así evitar complicaciones.



Finalizando


Hemos visto una de las formas (hay otras) de crear un proyecto, de definir un programa principal, archivos de inclusión y una estructura transportable usando rutas relativas. Lo único que queda es crear el resto de los componentes (forms, clases, reportes, etc) desde la ventana del proyecto, pero eso ya lo dejo a vuestro criterio.


Hasta la próxima!



viernes, enero 24, 2014

Nueva versión v1.19.4 de FoxBin2Prg

Por: Fernando D. Bozzo

Acabo de publicar la nueva versión de mantenimiento con los siguientes cambios:

- Nuevo parámetro "Recompile" para recompilar desde el directorio del PJX (si se proporciona)
- Cambio de funcionalidad: Ahora FoxBin2Prg no recompila los binarios por defecto, porque lo hace desde el directorio del binario y eso puede causar errores de compilación. Use el nuevo parámetro o recompile por su cuenta
- DBC: Agregado soporte multilínea para la propiedad "comment"
- VBS Batch scripts: Agregado progress bar

Se puede descargar de VFPX:
https://vfpx.codeplex.com/wikipage?title=FoxBin2Prg_es

Saludos!

martes, enero 14, 2014

Desmitificando el control de errores con Try/Catch

Por: Fernando D. Bozzo

He visto en muchas ocasiones que hay como cierto miedo a tratar los errores, como si fuera algo complejo, difícil de controlar o directamente como si los errores "no fueran a ocurrir", que es peor, porque no se controlan y se confía ciegamente en la suerte.

En esta entrada voy a presentar una forma de trabajar con el Try/Catch que cumple dos objetivos distintos con una misma rutina:
  1. Poder relanzar el error para que lo trate un nivel superior
  2. Poder tratar el error localmente sin relanzarlo
Y lo mejor de todo es que este comportamiento se puede elegir "desde fuera" del procedimiento, antes de llamarlo, de modo que un mismo procedimiento se puede usar de las dos formas, dependiendo de cómo se utilice.

¿Pero qué sentido tiene poder hacer esto? ¿Para qué puede servir llamarlo de una forma u otra?


Imaginemos dos situaciones distintas:

Situación 1: Tenemos una rutina en la que si ocurre un error, queremos mostrarlo para que el usuario esté notificado y nada más

Situación 2: Tenemos un método de negocio (procedimiento sin interfaz ni mensajes que se muestren al usuario que realiza alguna operación), donde de ocurrir un error, y al no poder mostrarlo, la única salida es reenviarlo hacia el nivel anterior para que sea tratado en el origen, por ejemplo, el botón de comando que originó todo, o un programa externo.

Normalmente esto requiere dos tratamientos distintos, pero hay una solución muy simple que permite utilizar una u otra de acuerdo a lo que nos convenga, que es la siguiente:

PROCEDURE Proc_X( toEx as Exception, tlRelanzarError, otros_params )
   TRY
      toEx = NULL
      * Aquí va el proceso
 

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF

   FINALLY
      * Este IF/MESSAGE/ENDIF sólo debe ir si este es un método de Interfaz!

      IF NOT ISNULL(toEx) AND NOT tlRelanzarError
         MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
      ENDIF

      * Recolección de basura
   ENDTRY
ENDPROC




En azul y amarillo están resaltados los dos parámetros que permiten esto. Por un lado, toEx guarda la información del error y por el otro tlRelanzarError indica si se desea, o no, relanzar el error al nivel superior. No hay más! Y así de simple como se ve, es sumamente versátil.

Veamos algunos ejemplos en la práctica.


Ejemplo 1: Tratamiento de errores local

En este caso se quiere controlar el error y mostrar un mensaje al usuario. Los métodos de este tipo suelen ser de la interfaz visual, y como se ve, es parecido al ejemplo original, pero más recortado y sin la parte de relanzar:


PROCEDURE Form1.cmdEjecutar.CLICK
   TRY
      LOCAL loEx as Esception
      loEx = NULL
      * Aquí va el proceso
   CATCH TO loEx

   FINALLY
      IF NOT ISNULL(loEx)
         MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
      ENDIF

      * Recolección de basura
   ENDTRY
ENDPROC




Dentro de este mismo tipo de tratamiento tenemos otro caso, una actualización que solo actualiza un campo TIMESTAMP que se llama con un ON KEY LABEL F2 y que, por llamado con una tecla ON KEY, no permite otro tratamiento de errores que no sea local:

ON KEY LABEL F2 DO Actualiza WITH "", .F., 0

PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
   TRY
      LOCAL lnCodError
      lnCodError = 0 
      toEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF
      lnCodError = toEx.ErrorNo

   FINALLY
      IF NOT ISNULL(toEx) AND NOT tlRelanzarError
         MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
      ENDIF

      USE IN (SELECT("USUARIO"))
   ENDTRY
   RETURN lnCodError
ENDPROC



Como se ve, desde el DO Actualiza se está indicando .F. para tlRelanzarError, por lo que en el FINALLY, si ocurrió un error, se mostrará el mensaje con sus datos. Además en el tercer parámetro se pasa 0 porque no se podrá leer su valor.




Ejemplo 2: Tratamiento de errores en nivel superior

En este caso vamos a usar los ejemplos anteriores, pero encadenados de tal forma que el segundo (la actualización) cambia su forma de trabajar para devolver el error y dejar que que sea el nivel superior quien muestre los datos del mismo:


PROCEDURE Form1.cmdEjecutar.CLICK
   TRY
      LOCAL loEx as Esception, lnCount
      loEx = NULL
      DO Actualiza WITH "", .T., lnCount
      IF lnCount = 0
         MESSAGEBOX( "No se actualizaron registros!" )
      ENDIF

   CATCH TO loEx

   FINALLY
      IF NOT ISNULL(loEx)
         MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
      ENDIF

      * Recolección de basura
   ENDTRY
ENDPROC


PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
   TRY
      LOCAL lnCodError
      lnCodError = 0 
      toEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF
      lnCodError = toEx.ErrorNo

   FINALLY
      IF NOT ISNULL(toEx) AND NOT tlRelanzarError
         MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
      ENDIF

      USE IN (SELECT("USUARIO"))
   ENDTRY
   RETURN lnCodError
ENDPROC



En este caso, si ocurriera un error en la rutina "Actualiza" se relanza el error (THROW) y se haría la recolección de basura (cerrar la tabla, limpiar vars, etc) tanto haya error o no.
El Throw salta hasta el CATCH del nivel anterior, que es el cmdEjecutar.Click() y recién ahí es donde se mostraría.

Así es como la misma rutina "Actualiza" se puede reutilizar controlando el error de dos formas distintas.


Control de errores compacto


Habiendo visto los casos de uso anteriores surgen varias ideas y optimizaciones para casos más concretos, por ejemplo, si sabemos de antemano que un método jamás mostrará un error porque es de negocio o para un proceso batch, entonces la estructura podría cambiar a esto:

PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
   TRY
      LOCAL lnCodError
      lnCodError = 0 
      toEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF
      lnCodError = toEx.ErrorNo

   FINALLY
      USE IN (SELECT("USUARIO")) 

   ENDTRY

   RETURN lnCodError
ENDPROC




Lo que sigue dejando 2 opciones:
  1. Llamar al método Actualiza relanzando el error, que lo redirigirá al Catch del nivel superior
  2. Llamarlo sin relanzar el error para evaluarlo localmente, como el siguiente caso

DO Actualiza WITH loEx, .F., lnCount
IF NOT ISNULL(loEx)
   *-- Ocurrió un error, hacer algo
ELSE
   *-- Ejecución OK
ENDIF




Achicando aún más


Finalmente hay una forma aún más compacta, asumiendo que solo se quiere relanzar el error y nunca se tratará como el caso anterior, sino directamente en el Catch del nivel superior que corresponda:

PROCEDURE Actualiza( tnCount )
   TRY
      LOCAL loEx as Exception
      lnCodError = 0 
      loEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO loEx
      THROW
   FINALLY
      USE IN (SELECT("USUARIO"))   ENDTRY
   RETURN
ENDPROC







Ayuda para Depurar en el control de errores


Esto es algo que no suele tenerse en cuenta, y que debería, ya que permite encontrar los errores mucho antes.
Cuando ocurre un error y se relanza la excepción hacia los niveles superiores, se pierde la información del entorno donde ocurrió (tablas abiertas, variables creadas, etc), ya que cada nivel fue cerrando y limpiando sus cosas, entonces se hace necesario una forma rápida que permita poder depurar "in-situ" cuando se está ejecutando en modo Desarrollo. Esta es una forma de hacerlo:

PROCEDURE Actualiza( tnCount )
   TRY
      LOCAL loEx as Exception
      lnCodError = 0 
      loEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO loEx
      IF _VFP.StartMode = 0 THEN
         SET STEP ON
      ENDIF
      THROW
 

   FINALLY
      USE IN (SELECT("USUARIO")) 

   ENDTRY

   RETURN
ENDPROC





Es así de simple y muy efectivo. Incluso se podría encapsular ese bloque en una función externa reutilizable, que también se encargue de generar un Log.


Una cosa importante sobre THROW:

No es lo mismo esto:

THROW


que esto:

THROW loEx



El primero relanzará el error tal cual, y se podrán seguir leyendo sus datos en loEx en cada Catch por el que pase, mientras que el segundo está relanzando específicamente un objeto Exception que al Catch de nivel superior le llegará en la propiedad UserValue, lo que lo hace más difícil de tratar si no se sabe esto previamente.


Información de error complementaria muy útil

Hay una propiedad del objeto Exception que suele pasarse por alto y que resulta ser muy útil para agregar información específica, que es UserValue. Obviamente que para aprovecharla se deben relanzar los errores con Throw solamente, o de otro modo se pisará su valor con una Excepción.

Lo que le da un valor especial es que al ser una propiedad "para el usuario" podremos poner allí lo que querramos, tanto un valor como un objeto como un XML, lo que sea, y esto permite agregar más información específica del sitio donde ocurrió el error.

Por ejemplo, en el procedimiento AnalizarBloque_CDATA_inline() de FoxBin2Prg encontramos esto:

CATCH TO loEx
    IF loEx.ErrorNo = 1470    && Incorrect property name.
        loEx.UserValue    = 'PropName=[' + TRANSFORM(tcPropName) + '], Value=[' ;

           + TRANSFORM(lcValue) + ']'
    ENDIF



En este caso concreto me resulta muy importante adjuntar al objeto del error el nombre y el valor de la propiedad que puede causarlo, ya que si no, no saldría reflejado en ningún otro sitio o a lo sumo saldría solo el nombre de la propiedad.

Luego, en el nivel superior, junto la información de esta forma para mostrarla o loguearla a un archivo:

lcError = 'Error ' + TRANSFORM(toEx.ERRORNO) + ', ' + toEx.MESSAGE + CR_LF ;
    + toEx.Procedure + ', ' + TRANSFORM(toEx.LineNo) + CR_LF ;

    + toEx.LineContents + CR_LF + CR_LF ; 
    + toEx.StackLevel + CR_LF + CR_LF ; 
    + toEx.Detailt + CR_LF + CR_LF ;
    + EVL(toEx.UserValue,'')



En el método escribirArchivoBin() de la clase c_conversor_prg_a_dbf, guardo datos sobre el índice, el campo y la sentencia de creación de la tabla, ya que se intentará crear en ese método, pero si falla necesito saber qué ocurrió:

loEx.UserValue = 'lcIndex="' + TRANSFORM(lcIndex) + '"' + CR_LF ;
    + 'lcFieldDef="' + TRANSFORM(lcFieldDef) + '"' + CR_LF ;
    + 'lcCreateTable="' + TRANSFORM(lcCreateTable) + '"'




Para cada caso se debe contemplar adjuntar información específica para que ubicar el error, en caso de ocurrir, se haga lo más rápido posible.


Conclusión

Vimos algunas técnicas para controlar los errores, que es una de las cosas más importantes en cualquier aplicación, ya que de ello depende que aparezca un error descontrolado y que el usuario decida si "Aceptar, Cancelar o Ignorar" (lo peor que puede pasar, sobre todo si "Ignora"), o que el sistema tenga el flujo de errores definido desde el método más profundo para reenviarlo hasta llegar la interfaz o al programa externo.

FoxBin2Prg, por ejemplo, usa el control compacto de errores, ya que no me interesa mostrar mensajes en cualquier momento, e incluso se puede configurar para no mostrar ningún mensaje y opcionalmente generar un log.

La recolección de basura también es muy importante, ya que no hacerlo de forma correcta puede llevar a un sistema inestable o que a la larga genere el temido error C0000005

Hasta la próxima!


Relacionados:
Técnicas de programación en VFP: Procesos desatendidos y control de progreso visual

sábado, enero 11, 2014

¿Cómo usar una tabla FoxPro varias veces sin que se cierre?

[2015-04-24 Actualizado con nuevo ejemplo]

Por: Fernando D. Bozzo

El comando USE es muy potente y tiene varios parámetros que están muy bien explicados en la ayuda.

Normalmente en un sistema multiusuario las tablas se abren compartidas (SHARED) para que varios usuarios puedan usar la misma tabla a la vez, y además muchas veces se dá la necesidad de abrir una misma tabla más de una vez en diferentes lugares (forms, clases, programas, etc), y la cláusula AGAIN y ALIAS existen con ese fin:


USE <Tabla> SHARED AGAIN ALIAS Tabla_1


Muchos programadores no suelen anticipar el uso multiusuario de una tabla y simplemente la abren SHARED creyendo que eso es todo... pero luego ven que en un momento dado uno de los sitios que usan la tabla la cierra, y el otro que usaba lo mismo falla porque no la encuentra abierta, ¿qué hacer entonces?

Ayuda mucho tener una nomenclatura para las variables y los ALIAS, de manera que esto se puede resolver muy fácil.



Dos o más formularios usando la misma tabla


Esta es una situación muy habitual, tener varios formularios que usan la misma tabla simultáneamente y hasta puede que con distintos índices en cada sitio, pero compartiendo los mismos datos.

Una forma de poder aprovechar esos datos e independizarse de qué haga el resto del sistema con ella es esta:


* Form Clientes usando la tabla Precios
USE PRECIOS SHARED AGAIN ALIAS cli_PRECIOS

* Form Proveedores usando la tabla Precios
USE PRECIOS SHARED AGAIN ALIAS prv_PRECIOS


¿Y si la tabla se abre en el entorno de datos (DataEnvironment)?
Fácil, se abre el entorno de datos, se elige la tabla PRECIOS y en la ventana de Propiedades se le cambia el ALIAS como se indicó antes.

Otra opción es usar sesiones de datos privadas, de esa forma se puede usar el mismo nombre de tabla sin cambiarle el alias.



Rutinas de consulta (SQL, LOCATE, SEEK, etc)


Cuando se separa un sistema en N capas (por ejemplo: DATOS, INTERFAZ, NEGOCIO), se hacen los métodos de consulta por separado y generalmente en clases específicas para accesos a datos. Es en estos métodos donde suele haber también el mismo problema que con los forms, tablas que se abren en distintos métodos para hacer distintas consultas, y que si están bien encapsulados (aislados) deberían abrir la tabla, hacer la consulta, pasar los datos a una variable o array, cerrar la tabla y devolver los datos.

Esta sería una forma de método de consulta encapsulado y optimizado:

PROCEDURE consultarPrecioPorArticulo
   LPARAMETERS tcCodArticulo, taPrecio

   TRY
      LOCAL lnSelect, loEx as Exception
      lnSelect = SELECT()
      SELECT 0

      USE PRECIOS SHARED AGAIN NOUPDATE ALIAS __Precios

      SELECT Precio FROM __Precios ;
         WHERE CodArtic == tcCodArticulo ;
         INTO ARRAY taPrecio

   CATCH TO loEx
      THROW

   FINALLY
      USE IN (SELECT("__Precios")) && Siempre cierro la tabla
      SELECT (lnSelect) && Restauro el área de trabajo original
   ENDTRY
ENDPROC


En este caso, y por el carácter temporal de la apertura (abre, consulta, cierra) antecedo el ALIAS con uno o dos guiones bajos, que es lo que suelo usar en estos casos. También notar el NOUPDATE en este caso, que hace una apertura ReadOnly (de solo lectura), que es más eficiente y rápido que el de escritura cuando se sabe que no se quieren actualizar datos.



Encapsulación en métodos de Negocio


Un sitio donde es muy útil usar este mecanismo de USE AGAIN es en los métodos de negocio para favorecer la encapsulación.

Seguramente muchos conozcan esta forma de abrir tablas o reutilizar las ya abiertas:

LOCAL llAbiertaAqui
IF NOT USED("MITABLA")
   USE MITABLA IN 0
   llAbiertaAqui = .T.
ENDIF
SELECT MITABLA
SET RODER TO INDICE

*...(el código del método)

IF llAbiertaAqui
   USE
ENDIF



Esta simple rutina adolece de varios problemas, veamos:
  1. Abre la tabla sin indicar el tipo de acceso deseado (EXCLUSIVE, SHARED, NOUPDATE, etc)
  2. ¿Qué ocurre si al intentar reutilizar la tabla que había abierta, la misma estaba como Sólo-Lectura y resulta que vamos a necesitar usar REPLACE? => Error en puerta!
  3. Vean todo el código necesario solamente para usar una tabla!
  4. ¿Y qué ocurre si dentro de "el código del método" llamamos a otro procedimiento que nos cambia el área de trabajo o el puntero de registro? => Otro error!
  5. Cuando reusemos la tabla que había abierta y cambiemos el puntero del registro, por ejemplo con un Go Bottom, Scan, Locate, etc, ¿qué puede ocurrir al retornar al método llamador? => Otra posibilidad más de error!
  6. Estamos estableciendo un índice, con lo que al retornar al módulo llamador, si olvidamos poner el que había.... otro error!

Como se ve, no es buena idea usar esto en los procedimientos, ya que pueden ocurrir varios errores de distinto tipo, pero en todo caso requerirá controlar varias situaciones más que usando simplemente esto:

SELECT 0
USE MITABLA SHARED AGAIN ALIAS _MITABLA ORDER TAG INDICE
 
*...(el código del método)

USE IN (SELECT("_MITABLA"))

...¿a que es mucho menos código? :D

Pero además evitamos otros problemas:

- Al usar otro alias, el puntero de registro original no lo estamos modificando y nos ahorramos el tener que guardarlo y restaurarlo
- Abrimos la tabla con el tipo de acceso que nos interesa (Escritura, Solo-Lectura, etc)
- Abrimos la tabla con el índice que nos interesa, sin tener que restaurar el original
- ...y todo esto sin afectar a la tabla abierta anteriormente, ni al módulo llamador, ni utilizando un handle (manejador) de archivo extra, ya que Fox sabe si la tabla la tiene abierta en otro sitio, y reutiliza el handle.





Resumen


Las cláusulas SHARED, AGAIN y ALIAS permiten usar múltiples veces la misma tabla con diferentes nombres, además eso permite que cada sitio abra la tabla con el índice que requiera, tiene su propio puntero de registro que no será afectado por otros programas o métodos y no consume recursos extra, por lo que es la solución ideal en todos los casos.


Hasta la próxima! :D

viernes, enero 10, 2014

FoxBin2Prg, el sucesor mejorado del scctext

[18/04/2015: Actualizado hasta la versión v1.19.42]

Por: Fernando D. Bozzo

Creo que sería un buen comienzo empezar este blog con una nota sobre FoxBin2Prg, un proyecto Open Source actualmente hospedado en CodePlex dentro del proyecto VFPX y que comencé a fines de noviembre de 2013, por la necesidad de no solo generar vistas texto de los binarios de Visual FoxPro 9 como ya hace el scctext, sino de mejorarlo a tal punto que para el Desarrollador sea como el código mismo, y que además permita ser usado para hacer Diff, Merge y de paso Backup del código.


Ventajas:

  • Genera archivos estilo "PRG" (no compilables), para comparación visual
  • Permite hacer cambios en la versión TEXTO tan fácil como modificar PRG
  • Todo el código de programa está en un solo EXE, para simplificar su copia y mantenimiento
  • Con las versiones TEXTO puedes regenerar los binarios originales, así que es útil como backup
  • Las extensiones usadas son configurables si se crea el archivo FOXBIN2PRG.CFG
  • Los métodos y propiedades de la versión TEXTO son ordenados alfabéticamente para acilitar su comparación
  • Tiene compatibilidad con el SccText(X) a nivel de parámetros, así puede ser usado como su sustituto con SourceSafe

Actualmente se soporta la conversión de archivos PJX,SCX,VCX,FRX,LBX,DBC,DBF y MNX, para los que genera versiones Texto con extensión PJ2,SC2,VC2,FR2,LB2,DC2,DB2 y MN2 que pueden ser reconfiguradas para compatibilizar con SourceSafe.

Ejemplo de archivo de configuración FOXBIN2PRG.CFG si necesita cambiar extensiones
extension: SC2=SCA
extension: VC2=VCA
extension: PJ2=PJA
...


Usando la versión "EXE": (útil para ser llamado por programas de 3ros)
FOXBIN2PRG.EXE "<path>\file.scx" ==> Genera la versión TEXT con extensión sc2
FOXBIN2PRG.EXE "<path>\file.sc2" ==> Regenera el binario con extensión scx


Usando la versión "PRG":
DO FOXBIN2PRG.PRG WITH "<path>\file.scx" ==> Genera la versión TEXT con extensión sc2
DO FOXBIN2PRG.PRG WITH "<path>\file.sc2" ==> Regenera el binario con extensión scx


Usando la versión "Objeto":
LOCAL loCnv AS c_foxbin2prg OF "FOXBIN2PRG.PRG"
loCnv = NEWOBJECT("c_foxbin2prg", "FOXBIN2PRG.PRG")
loCnv.Ejecutar( <params> )


El código generado


Para cada tipo de binario Fox hay una vista personalizada, pensada para sacarle el mayor provecho a cada uno. Por ejemplo, para los DBFs y DBCs (tablas y bases de datos nativas) se generan vistas estilo XML



y para el resto se genera código de programa y seudocódigo, que no compila, pero es tan fácil de leer como el código normal, con la ventaja de que en todos los casos se pueden hacer modificaciones que luego se trasladarán al binario regenerado.


Estructura de clases



Todo el código está en un único programa dividido en varias clases:
  • Una clase principal llamada c_foxbin2prg, de tipo session, que es la que recibe los parámetros
  • Una para el indicador de avance cuando se procesan varios archivos llamada frm_avance
  • Una para cada tipo de conversión específica, que es cargada por la clase principal y que están basadas o en c_conversor_bin_a_prg o en c_conversor_prg_a_bin
  • Unas cuántas de soporte basadas en cl_col_base y cl_cus_base para poder completar los datos de cada unidad o subunidad de información.
Por ejemplo, el conversor de DBF a texto, además de la clase conversora principal, utiliza:
  • Una clase para la información del DBF (cl_dbf_table)
  • Una clase para la información de campos (cl_dbf_field)
  • Una clase para la información de índices (cl_dbf_index)
  • 2 clases de colección (cl_dbf_fields y cl_dbf_indexes)

Funcionamiento


Cuando se pide convertir un VCX a VC2, ocurre lo siguiente:
  • La clase c_foxbin2prg recibe los parámetros de entrada en el método execute(), siendo el primero de ellos el nombre y ruta del archivo a convertir. En este paso también se hacen algunas validaciones
  • A continuación se llama al método convert(), que selecciona el conversor a utilizar de acuerdo al tipo de archivo pasado, en este caso es c_conversor_vcx_a_prg, se instancia la clase, se le pasan los datos necesarios y se invoca a su método Convertir()
  • El método Convertir() abre la librería VCX como una tabla en modo SHARED NOUPDATE con alias TABLABIN y a partir de allí se recorren todos los registros, invocando a métodos intermedios específicos para tipo de dato del VCX que irán analizando los datos del registro e irán generando cada uno su parte de información de texto, como la definición de la clase, el ordenamiento alfabético y definición de las propiedades al inicio, el ordenamiento alfabético y definición de los métodos, etc.
En los distintos métodos se va obteniendo, formateando y escribiendo trozos de código, y todo el código/texto generado se va guardando en una variable pública llamada C_FB2PRG_CODE, que al finalizar se escribe en el archivo VC2.

Cuando se pide regenerar un binario desde el VC2, ocurre lo siguiente:
  • Se realizan los dos primeros pasos del proceso antes descripto, pero instanciando la clase c_conversor_prg_a_vcx
  • En el método convert() se invoca a identifyExclusionBlocks() que analiza el código del VC2 buscando los #IF .F. y los TEXT..ENDTEXT, para saber que dentro de esos bloques no debe analizar nada
  • Se llama al método identifyCodeBlocks() que a su vez llama a los distintos métodos analyze que se encargan de analizar secciones específicas de código como los ADD_OBJECT, DEFINED_PAM (las propiedades, arrays y métodos definidos por el usuario), HIDDEN, INCLUDE, METADATA y demás secciones del código, cada una de las cuáles realiza un parseo específico para completar las propiedades del objeto de datos que le corresponde
  • Finalmente se invoca al método writeBinaryFile() que recorre toda la información recolectada antes y escribe el binario

Control de errores


El control de errores se basa en Try/Catch, donde cada método controlado se encarga de 2 cosas:
  1. Tratar el error, generar la información de Log necesaria y limpiar el entorno modificado (garbage collect)
  2. Relanzar el error hacia el nivel superior, que llegará finalmente hasta la capa más externa que es la clase principal, donde dependiendo del modo de llamada de foxbin2prg (EXE, PRG u Objeto) devolverá un tipo de información u otro.
Por ejemplo, en modo EXE se devuelve un código de error 1 como resultado de ejecución, valor que puede ser leido por un BAT o un script VBS con ERRORLEVEL para tomar alguna decisión o realizar alguna acción.


Parámetros


Finalmente, los distintos parámetros de entrada permiten configurar cómo se desea que funcione y actúe ante ciertas situaciones:
  • tc_InputFile: Nombre completo del archivo de entrada a convertir
  • tcType_na: Mantenido por compatibilidad con SourceSafe. Por ahora sin uso.
  • tcTextName_na: Mantenido por compatibilidad con SourceSafe. Por ahora sin uso.
  • tcGenText_na: Mantenido por compatibilidad con SourceSafe. Por ahora sin uso.
  • tcType_na: Mantenido por compatibilidad con SourceSafe. Por ahora sin uso.
  • tcDontShowErrors: Un "1" indica que no se quiere mostrar errores si llegaran a ocurrir. Esto es útil en un procesamiento batch, donde no se quiere que los errores interrumpan el proceso ya que se loguean a un archivo .LOG o .ERR que se puede revisar luego
  • tcDebug: Un "1" indica habilitar el modo de depuración, que solo se usa en modo desarrollo cuando se ejecuta el PRG y que permite que se abra una ventana de DEBUG para depurar el error en el mismo sitio donde ocurrió, inspeccionar las variables y ver lo que sea necesario. En tiempo de ejecución (EXE) solo habilita el modo de generación de LOG, que también se puede habilitar creando un archivo FoxBin2Prg.log junto al EXE
  • tcDontShowProgress: Un "1" indica que no se quiere mostrar una barra de progreso cuando se procesa un lote de archivos. Para un único archivo no se muestra esta barra de progreso.
  • tcOriginalFileName: Este parámetro fue uno de los últimos agregados, y está especialmente pensado para trabajar con herramientas SCM, donde la mayoría de las veces se generan los archivos en ubicaciones temporales (c:\temp) y con nombres temporales (fffds-44343-ffds5-fsdfe.vcx). Gracias a este parámetro el conversor sabe el nombre original del archivo y lo puede utilizar en sitios donde queda registrado, como la cabecera de los TX2 y algunas propiedades importantes. Por ejemplo el PJ2 tiene unas cuántas referencias a su nombre en distintas propiedades, que sin este parámetros tendrían un nombre temporal que impediría regenerar el binario con el nombre correcto.
  • tcRecompile: Indica si se debe recompilar, o no. Por defecto, desde la v1.19.4 ya no recompila, salvo que se indique '1' para recompilar desde el directorio del binario (como hacía antes) o un 'Path' para recompilar desde el mismo. Lo mejor es undicar el Path del proyecto PJX, ya que siempre que se usa un proyecto, los componentes que se agreguen se compilan desde esa ubicación relativa, y todos los arhivos se vinculan de forma relativa, como los Include (..\include\foxpro.h), que se se compilaran desde otra ubicación darían errores de compilación, que es lo que ocurría con el funcionamiento anterior de FoxBin2Prg cuando se referenciaban archivos externos de forma relativa.

 Link de descarga:
https://github.com/fdbozzo/foxbin2prg

Conclusión

Omití varios detalles para no hacer el post muy largo, pero quería explicar un poco por encima el funcionamiento de este programa y su estructura. Probablemente en próximos posts hable sobre algunas particularidades más específicas y de algunas técnicas de programación utilizadas.

Hasta la próxima!


Artículos relacionados:
FoxBin2Prg: Detalle de vistas, datos de uso, configuraciones y más