18 de noviembre de 2017

Obtener la ruta (path) en formato largo

En alguna ocasión puede que obtengamos un nombre de archivo en formato corto, por ejemplo "c:\Docum~1\Usuario\Misdoc~1\" y convertirlo a "c:\Documents and Settings\Usuario\Mis documentos\". A continuación una función API para llegar a ello.

DECLARE integer GetLongPathName IN WIN32API ;
 string @ lpszShortPath, string @ lpszLongPath, integer cchBuffer

#define MAXPATH 267
STORE SPACE(MAXPATH) TO lpszLongPath
lcPath = "C:\DOCUME~1\epalma\Misdoc~1\"
lnLen = GetLongPathName(m.lcPath,@lpszLongPath,MAXPATH)
if lnLen > 0
 ? SUBSTR(lpszLongPath,1,lnLen)
else
 ? Ruta Inválida'
endif

Çetin Basöz
MS Foxpro MVP, MCP

12 de noviembre de 2017

Calcular el dígito de verificación para Rapipago y Pagofacil (Argentina)

En Argentina existen al menos dos redes de cobranzas extrabancarias (Rapipago y Pagofacil) que permiten el pago de facturas, servicios, etc. a través de formularios con códigos de barras que se emiten con una estructura particular para cada caso.

En primer lugar, a la cadena a codificar se le deben agregan 1 ó 2 dígitos de verificación al final. El algoritmo (en código VFP) para la generación de estos dígitos es el siguiente:

CLEAR 

lc = "1234567890123456789012345678901234567890"
? "Sin dígito de verificación...: " + lc
? "Con 1 dígito de verificación.: " + GenerarDigitoVerificador(lc, 1)
? "Con 2 dígitos de verificación: " + GenerarDigitoVerificador(lc, 2)

FUNCTION GenerarDigitoVerificador(tcCadena, tn)
  *---
  * Parámetros
  *   tcCadena = Cadena a generar el/los dígito/s de verificación
  *   tn = Cantidad de dígito/s de verificación (1 ó 2)
  * Retorno:
  *   Caracter: La cadena mas el/los dígito/s de verificación
  *---

  IF EMPTY(tn) OR NOT INLIST(tn, 1, 2)
    tn = 1
  ENDIF

  tcCadena = ALLTRIM(tcCadena)

  LOCAL lnLen, lnIni, lnSum, lcSeq, lcRet
  lnLen = LEN(tcCadena)
  lcSeq = "1" + REPLICATE("3579", CEILING(lnLen/4))
  lnSum = 0
  FOR lnIni = 1 TO lnLen
    lnSum = lnSum + VAL(SUBSTR(tcCadena, lnIni, 1)) * VAL(SUBSTR(lcSeq, lnIni, 1))
  ENDFOR
  lcRet = tcCadena + TRANSFORM(MOD(INT(lnSum / 2), 10))
  IF tn = 2
    tcCadena = ALLTRIM(lcRet)
    lnLen = LEN(tcCadena)
    lcSeq = "1" + REPLICATE("3579", CEILING(lnLen/4))
    lnSum = 0
    FOR lnIni = 1 TO lnLen
      lnSum = lnSum + VAL(SUBSTR(tcCadena, lnIni, 1)) * VAL(SUBSTR(lcSeq, lnIni, 1))
    ENDFOR
    lcRet = tcCadena + TRANSFORM(MOD(INT(lnSum / 2), 10))
  ENDIF
  RETURN lcRet
ENDFUNC

Luego de añadir los dígitos de verificación, ya se puede generar el código de barra Interleved 2 of 5 (I2of5) o Código 128 C, que son los que utilizan ambas empresas.

Si desean realizar toda la tarea con Visual FoxPro, pueden utilizar la clase FoxBarcode que soporta ambas simbologías de códigos de barra. Si utilizan el código I2of5 se debe configurar la propiedad lAddCheckDigit que no genere el dígito de control: (lAddCheckDigit = .F.)

Luis María Guayán

7 de noviembre de 2017

Introducción a Cliente-Servidor

Articulo original: Introduction to Client-Server
https://www.tedroche.com/Present/1999/CS.html
Autor: Ted Roche
Traducido por: Esparta Palma

Introducción

La arquitectura Cliente-Servidor parece ser muy simple en los ejemplos: cree una conexión, cree una vista remota hacia los datos, y estará en el negocio, correcto? . En esta sesión, Ted se introducirá en la potencia detrás del GUI, con la revisión de los comandos y funciones necesarias para crear un sistema Cliente-Servidor real. SQL Pass-throuhg, control manual de transacciones, manipulación de propiedades en conexiones, vistas y cursores, procesamiento en lote, precompilación y muchos otros temas que necesitará para la lucha con un sistema real a ser considerado.

Tecnologías Cliente Servidor y Visual FoxPro

Este pequeño documento no pretende ser una referencia completa hacia todo lo relacionado con Cliente-Servidor. En vez de eso, su intención es de servir como una pequeña introducción hacia los conceptos y las técnicas usadas para realizar una aplicación Cliente-Servidor con Visual FoxPro. Los lectores deberán estar familiarizados con Visual FoxPro y con el uso del contenedor de Base de Datos de FoxPro (DBC) y las vistas trabajando con datos locales. Empezaremos con algunas simples definiciones de los términos, revisaremos por qué deberías estar con Cliente-Servidor, y entonces cavaremos en los comandos y funciones para hacerlo posible. Concluiremos con algunas discusiones acerca de técnicas más avanzadas y cómo debería considerar la integración de Cliente-Servidor en su Framework.

¿Qué es Cliente-Servidor?
Hay una gran cantidad de definiciones acerca de qué es exactamente “Cliente-Servidor”, existen mas o menos estudiantiles, mas o menos robustas. Un vendedor hace algunos años diría: ¿“Cliente-Servidor”?, bueno, eso es cualquier cosa que debo vender ahora. Típicamente, para nuestros propósitos, el elemento clave de Cliente-Servidor es que dos procesos separados van a ser ejecutados, uno con Visual FoxPro, y otro que proveerá datos a Visual FoxPro. Estos dos programas pueden ser ejecutados tanto en la misma máquina como en forma separada, esto con algún tipo de conexión entre ellos (red, puerto serial o Internet) para permitir la comunicación.

¿Por qué Cliente-Servidor?
Muchos clientes ven interesante al esquema Cliente-Servidor por razones que están más allá de los méritos técnicos. Esto es ciertamente válido, pero tenga cuidado con las trampas en las que pueda caer si la solución Cliente-Servidor no fue realizada por una razón técnica primaria. Hay tres razones primordiales a considerar en Cliente-Servidor:
  • Tamaño de Datos: Visual FoxPro, al igual que todas las variantes de xBase anteriores a él, tenia la limitación de 2 GB en cualquiera de sus tablas o archivos. Esta limitación tiene que ver con la manera en que los bloqueos son realizados en los registros individuales y, mientras que es ciertamente factible que este límite sea alcanzado, esto es diferente a cómo normalmente lo hace Microsoft. A pesar de que hay muchos workarrounds para estos límites, estos incrementan los retos con Visual FoxPro en cuanto las tablas se agranden – tiempos prolongados para reindexar en caso de corrupción, por ejemplo. En resumen, si el tamaño se vuelve un factor mayor, considere si el uso de Cliente-Servidor es adecuado.
  • Seguridad: Fundamentalmente, todos los accesos a las tablas FoxPro van a través de la red del sistema operativo, así que los usuarios necesitan tener acceso a los directorios conteniendo las tablas VFP. Cualquiera que pueda tener acceso a las tablas puede, eventualmente, imaginarse como leerlos. También es simple usar un driver ODBC y Excel, o si ellos necesitan usar un editor hexadecimal para romper su esquema de encriptación. Muchos sistemas Cliente-Servidor pueden eliminar esta amenaza en conjunto con la restricción de acceso de los clientes a la interface del servidor, y no necesariamente a todos los datos. Si esta tratando con material altamente confidencial, Cliente-Servidor tiene sentido por razones de seguridad.
  • Bajo Ancho de Banda: Visual FoxPro es el producto para manejo bases de datos escritorio y basado en LAN mas rápido y con mejor mejor desempeño disponible en el mercado hoy en día. Pero VFP obtiene su desempeño fenomenal tomando ventaja del ambiente LAN, pre-obteniendo información de columna, haciendo localmente algún tipo de caching de encabezados de tablas y contenidos de índices. Mientras que el proceso de adquirir esta información es casi imperceptible en un ambiente de red, haciendo lenta la apertura inicial de tablas en milisegundos, esto puede ser un retardo substancial si se está en un “cable delgado” (por ejemplo conexiones Dial-Up, WAN o Internet saturado) entre el cliente y los datos. En estas situaciones, poner los datos, la responsabilidad de hacer las consultas y procesamiento de los datos en el servidor minizará los costos de comunicación y mejorará la velocidad.


¿Por qué NO Cliente-Servidor?
Como se mencionó anteriormente, los clientes pueden estar decepcionados si ellos han seleccionado una arquitectura Cliente-Servidor y sólo encuentran que el producto resultante es menos que satisfactorio. Hay muchas razones a considerar para no escoger Cliente-Servidor:
  • Complicaciones de Recursos: Cliente-Servidor es fundamentalmente mas complejo que un single-tier o aplicación monolítica. Nuevas necesidades de aplicación deben ser aprendidas, con esta terminología extranjera, sintaxis y lenguaje. Connectividad, ODBC y temas de red necesitan ser tomadas en cuenta. Nuevas APIs pueden ser necesitadas y dominadas. Anomalías, desdichas y plantear viejos bugs en la interface del vendedor, además de la búsqueda de driver. Si su equipo es maximizado y agendado, no piense que Cliente-Servidor será una “bala de plata”.>
  • Mantenimiento en Curso: Mientras que los vendedores están tratando de hacer sus aplicaciones mas “Instálelo y Olvídelo”, no existen instalaciones “hands-free”. Un administrador de Base de Datos (DBA, Database Administrator) tiene que estar disponible, al menos por periodos, revisar logs, asegurarse que los respaldos estén trabajando, el espacio en disco sea adecuado, etc. En realidad, esto es un requisito para muchas instalaciones de computadoras, pero los server típicamente necesitan un poco de atención adicional. Asegure el factor de costo de un DBA como parte de su vida en Cliente-Servidor.
  • Inestabilidad de Infraestructura: La percepción entre la gerencia superior es que Cliente-Servidor es inherentemente mas robusto, y que una instalación de este tipo es menos propensa a caídas que una instalación FoxPro. De hecho, la estabilidad de la red del sistema operativo y sus servidores no tienen relación con el software ejecutándose en ellos, y una inestabilidad no supone que las caídas y requerimientos de las aplicaciones FoxPro para reindexar todos sus archivos van a requerir un servidor que vaya a través de los procedimientos de recuperación. Un mejor uso del esfuerzo en mucho de estos casos es reparar los servidores con problemas.
  • Rendimiento: Como condición de que el suficiente ancho de banda está disponible, el rendimiento por sí solo nunca es razón para considerar al Cliente-Servidor, a menos de que los clientes requieran que la aplicación funcione más lentamente. En hardware equivalente, Visual FoxPro ejecuta muy decentemente en comparación de un software de servidor de datos


¿Cómo funciona Cliente-Servidor?
La diferencia principal entre trabajar con datos locales (datos VFP nativos) y datos cliente servidor es la necesidad de establecer una conexión con el servidor. Después de que la conexión es establecida, una vista puede ser usada para leer y escribir datos. En adición, la equivalencia de las sentencias SQL nativas de VFP pueden ser usadas para proporcionar un control más directo que el usualmente disponible vía Vista. Finalmente, como casi todo en Visual FoxPro, existe otra capa de fino control disponible a los desarrolladores que lo requieran.

Conexiones

Una conexión usa porciones del Microsoft Open Dabatase Connectivity (ODBC) para establecer una conexión de dos vías de comunicación entre Visual FoxPro y el servidor destino. Cada vendedor tiene su propio mecanismo para proveer, instalar, configurar y soportar esos drivers. Una vez establecida, la conexión de FoxPro puede ser configurada. Hay cierta confusión en la aplicación de la terminología VFP con respecto a conexiones. Cuando usted crea o modifica una conexión usando su sintaxis correspondiente, está modificando la definición de una conexión, y no está afectando una conexión existente. Las conexiones son almacenadas en el DBC y las definiciones son usadas cuando son llamadas. Las conexiones FoxPro pueden ser definidas visualmente con el Database Container (DBC) o directamente desde el Command Window con CREATE CONNECTION. Situación similar sucede con los equivalentes a los comandos DELETE, MODIFY, RENAME y DISPLAY CONNECTION. Las conexiones pueden referirse a un ODBC Data Source Name (DSN), definidas con el Applet “Panel de Control ODBC”, o se pueden especificar directamente los parámetros equivalentes. Por código, la conexión podría lucir como el siguiente:

** La primera conexión depende de un DSN pre-establecido
DEFINE Connection cnTest  
DATASOURCE dsnAccessNWind          
DATABASE C:\Access\nWind.MDB

** La segunda conexión, una conexión sin DSN (DSN-Less)
DEFINE Connection cnVFPFile     
CONNSTRING "DRIVER={Microsoft Visual FoxPro};" + ;     
                        "SOURCETYPE=DBF;" + ;          
                   "SOURCEDB=C:\VS98\VFP98\



Finalmente, las conexiones no necesitan ser mantenidas en un DBC. Si el resto de el sistema está usando SQL Pass Through, las conexiones pueden ser creadas al vuelo, usando las funciones SQLConnect() y SQLStringConnect():
** Abrir una conexión usando un DSN, UserID y PassWord
lnHandle = SQLConnect("mySQLServerDSN","SA","")

** Conectar a una tabla via el Driver ODBC driver sin un DSN:
lnHandle = SQLStringConnect("DRIVER={Microsoft Visual FoxPro};"+;
                            "SOURCETYPE=DBF;"+ ;                                                 
                            "SOURCEDB=C:\VS98\VFP98\")



Vistas
Las Vistas Remotas no son muy diferentes a las vistas locales, pero hay un par de áreas que nos pueden preocupar. La primera preocupación es sobre los tipos de datos almacenados en la base de datos remota pueden no compararse exactamente con los tipos de Visual FoxPro. Por ejemplo, no hay concepto de tipo de datos Lógico o Booleano en el esquema de Oracle. Esto puede ser fácilmente resuelto usando el Diálogo “Propiedades del Campo” y especificar el tipo de datos deseado. Use esta opción para mapear campos DateTime a datos tipo Fecha. Los campos Fecha son poco comunes en backends de base de datos, pero las funciones matemáticas de fechas de VFP están en días y las tipo DateTime en segundos. Tenga cuidado de asegurarse que se usan las unidades de medida correctas. La segunda preocupación es uso apropiado de conexiones. En el diseñador de Vistas, bajo las Opciones Avanzadas -> Menú Opciones, en la sección “Query”, verá un checkbox etiquetado “Compartir Conexión” (“Share Connection” para la versión en inglés). Por el simple hecho de que las vistas utilizan la misma conexión, no significa que cada uno intentará compartir la conexión actual, por el contrario, cada vista creará su propia copia de la conexión definida a menos de que ese checkbox esté marcado. Hay una configuración global equivalente, esta se encuentra en Herramientas/Opciones/Datos Remotos, esto no hacía nada en VFP 3 y 5. Finalmente, en VFP 6, esta ocasionaba que la compartición de conexión fuera seleccionada por default cuando estaba marcada. Vea “Vistas y Conexiones” mas adelante para una discusión sobre los temas envueltos.

Al igual que las conexiones, las Vistas pueden ser definidas y manipuladas programáticamente, al igual que visualmente. Hay algunas ventajas reales al definir las vistas programáticamente. Es fácil para todos el ver la definición de la vista. No necesita meterse en problemas con el Diseñador de Vistas cuando las relaciones complejas no son visualizadas correctamente. Finalmente, puede extender facilemente la naturaleza del meta-data del sistema para generar la Vista al vuelo. Revise el GenDBC (incluido con Visual FoxPro en el directorio (HOME()+”Tools\GENDBC”) para algunas ideas al extraer meta-data desde el DBC o usarlo para generar los comando requeridos para regenerar el DBC.

El comando CREATE VIEW crea una vista remota en el contenedor de base de datos actual. El formato de el comando parece un poco extraño, debido a que el comando SQL SELECT contenido dentro del CREATE VIEW no está delimitado de ningún modo – no se le establece comillas o paréntesis o algo. Este comando SQL SELECT describe como obtener los datos para producir la Vista. Recuerde que Visual FoxPro no está dando los datos – una aplicación servidor lo hace. Así,el SQL usado en el comando CREATE VIEW es el SQL y su extensiones nativas al server, no así a Visual FoxPro. Esto significa que debería evitar “FoxProismos” o funciones integradas de FoxPro (TRIM(), STR(), VAL()), a menos de que sepa que son soportadas por el server. Hay una excepción hablando del lenguaje materno del servidor, y esta situación de pedirle al ODBC que traduzca algunas frase en el proceso. Recuerde que se está usando ODBC para conectar Visual FoxPro al driver de base de datos correcto. ODBC también provee una API e incluye la habilidad para traducir la requisición desde un SQL más genérico hacia uno entendible por el backend. Las funciones pedidas al ODBC se desactivan del código SQL al poner llaves (símbolos { } ). Estos códigos pueden incluir peticiones para funciones internas del ODBC, traducciones de ciertos datos en formatos fecha y tiempo, y hasta extensiones específicas soportadas por algunos drivers. Revise la documentación de su driver ODBC llamando el Applet ODBC y selecionando ayuda.
* Ejemplo de una vista definida por código:

CREATE VIEW l_Customers_State ;
   REMOTE CONNECTION cnOracle SHARED AS ;
   SELECT * ;
     FROM Customer ;
    WHERE Customer.State = &pcState ;
    ORDER BY Customer.Name
Vistas y Conexiones
La razón por la cual usted necesita entender cuantas conexiones establecer con el servidor es por que cada conexión consume recursos, recursos que tienden a ser preciosos para el rendimiento del server y tienden a limitar la escalabilidad del sistema entero. También, algunos vendedores tienen licencias de sus servidores por conexión, en vez de usuarios, así que esto podría convertirse en un problema de licencias. Entonces, ¿Por qué no compartir toda la comunicación con el server sólo con una conexión? Bien, hay algunas restricciones que hacen esto mas complicado al usar sólo una conexión. Primero, las conexiones son definidas en el DBC y sólo pueden ser usadas por vistas en el DBC. Si tiene múltiples DBCs en su aplicación (y hay una o muchas buenas razones para no hacer esto), entonces estará manteniendo múltiples conexiones. Segundo, mientras que los comandos SQL Pass Through pueden compartir una conexión con una vista, consultando su conexión y entonces usar el mismo manejador, una vista no compartirá una conexión [Nota del Editor: Esto ha cambiado con la llegada de Visual FoxPro 8, en esta versión si es posible compartir conexiones entre SPT y las Vistas Remotas]. Finalmente, los comandos están diseñados para tomar algún tiempo o que requiera de más de un simple comunicación de “Pedir-Responder” entre el Cliente y el Servidor, serán propensos a “bloquear la línea telefónica” y necesita estar en su propia conexión. Dichos comandos incluyen peticiones progresivas, comandos por lotes, comandos precompilados y muchas otras situaciones. Es estos casos, planee una o mas conexiones para los procesos en lotes, y también planee una conexión compartida, si es posible, una para vistas y otra para sentencias cortas en el lenguaje SQL. SQL Pass-Through (SPT) Como se mencionó en secciones previas, toda la funcionalidad que puede ser obtenida usando el Contenedor de Base de Datos y las herramientas visuales también está disponible programáticamente con el lenguaje. Como siempre, esto es una de los puntos destacados de Visual FoxPro, los cuales están bien implementados aquí:

Función Propósito
SQLTables() Retorna una lista de tablas o vistas para una conexión específica. Útil para utilerías de “caja negra”
SQLColumns() Retorna especificaciones de campos en uno de varios formatos, útil para comparar capacidades de VFP con base de datos externas.
SQLConnect(), SQLStringConnect() Descritos anteriormente, son usados para establecer una conexión con el servidor
SQLExec() Pasa comandos al server vía ODBC, estos serán ejecutados.
SQLCommit(), SQLRollback() Provee la funcionalidad para completar o abortar una transacción. La transacción debe ser empezada en modo manual, utilizando SQLSetProp() descrita más abajo.
SQLSetProp(), SQLGetProp() Lee y establece propiedades de una conexión existente.
DBSetProp(), DBGetProp() Lee y establece propiedades de un Contenedor de Base de Datos con respecto a vistas, conexiones, tablas, etc. Estos son los únicos valores persistentes entre sesiones.
CursorSetProp(), CursorGetProp() Establece o lee propiedades en un cursor abierto, incluyendo el buffering.


Cliente Servidor Avanzado
Puede que los detalles de estos tópicos estén lejos de lo que es apropiado cubrir en una sesión introductoria, exponer estos conceptos puede darle algunas ideas sobre de lo que puede considerar estos productos en aplicaciones más avanzadas:

Batch SQL: Múltiples estatutos SQL pueden ser enviados al servidor, con procesamiento contínuo en el cliente, y ocasionalmente revisar el estatus del procesamiento.
Progressive Fetching: Muy conveniente cuando se descarga un conjunto de datos potencialmente grande, este método retorna el control a FoxPro después de que una cantidad predeterminada de registros han sido devueltos, y automáticamente obtiene registros adicionales en medida de que se vayan necesitando. Un ejemplo excelente sería un Grid para tomar la lista de clientes, donde el operador podría especificar un criterio de búsqueda, y el cliente podría estar listado si está dentro del criterio. El operador podría recorrer la página en la lista, y los registros adicionales podrían ser obtenidos así sean necesitados.
Precompilación: La función SQLPREPARE() permite al servidor recibir una sentencia SQL a ejecutar, y realizar sus funciones sobre ella – revisión, análisis, desarrollo del plan de trabajo, etc. - y entonces ejecutar la operación mucho más rápido que cuando es llamado.
Entodos estos tres casos, estas funciones requerirán una conexión separada, ya que ellas “bloquearán la linea telefónica” como fue mencionado anteriormente en la sección de Vistas y Conexiones. Sin embargo, estas funciones proveen esta necesidad de conexión.

Sugerencias en el aprendizaje de Cliente-Servidor
Las aplicaciones Cliente-Servidor pueden verse como algo muy complicad, pero no son muy difíciles si se aprende una parte a la vez. Recomiendo aprender a usar la funcionalidad básica de ODBC con Visual FoxPro a través de la experimentación directa con otra tabla o base de datos en su máquina, usando el driver ODBC de VFP. Una vez que gana experiencia, intente con conexiones sin DSN (DSN-Less), conexión directas usando SPT, e intenta cancelar y aceptar varios cambios a los datos. Después de que se haya familiarizado con los comandos y funciones básicas con Visual FoxPro, mire en el software del server. Aprenda cómo instalarlo y configurarlo, si es necesario, y aprenda que componentes son requeridos para instalar conectividad con clientes y driver. Muchos productos vienen con una sencilla ventana de comandos o interface interactiva, úsela para verificar que la conectividad está trabajando correctamente. Puede ser bastante frustrante el tomar algo desconocido y buscar por uno mismo que es lo que no funciona en el sistema, más aún si no hay manera de saber que sucede debido a la mala especificación de protocolo de red, direcciones o puertos incorrectos, o si falta algo por configurar.

Soporte del Framework
Todos los desarrolladores deberían estar usando un framework consistente para desarrollar sus aplicaciones; en vez de empezar de la nada y reinventar la rueda, un framework puede proveer un buen punto de inicio para desarrollo, y un siempre creciente nivel de sofisticación en el servicio proveido. Una de las características clave que debería proveer un framework debería ser las rutinas de servicio para obtener y guardar datos. Visual FoxPro es ideal proveyendo soporte para frameworks de servicios de datos debido a esto hay mucha consistencia y ricos modelos para trabajar con datos. Seleccionando uno de estos modelos y usándolos consistentemente con su framework proveerá un robusto mecanismo de datos.

Conclusión Visual FoxPro provee excelentes herramientas para obtener un control completo de Cliente-Servidor, tanto en variedad gráfica, como a través de la línea de comando. La tecnología Cliente-Servidor es apropiada para tamaños de datos extremadamente grandes, para situaciones de seguridad de alto riesgo, y para ambientes con comunicaciones con bajo ancho de banda desde el cliente al servidor. Además de que el Diseñador de Vistas y el Diseñador de Conexiones pueden permitir un completo diseñado gráficamente, Visual FoxPro también provee las herramientas y funciones para mezclar diseños programáticos y gráficos(o hasta basado en metatablas). Esta combinación hace fácil de aprender combinando el poder en la implementación.

Acerca del Autor. Ted Roche es un Desarrollador Certificado por Microsoft y es el director de desarrollo en Blackstone Incorporated, una compañía catalogada como Microsoft Certified Solution Provider en Waltham, Massachussets, USA, donde el desarrolla aplicaciones complejas usando herramientas visuales y “BackOffice” de Microsoft. Él es un contribuidor de seis libros de FoxPro, los últimos esfuerzos de Ted han sido como coautor de “Hacker's Guide to Visual FoxPro 6.0” disponible en Hentzenwerke Publishing, así como también en librerías de prestigio. Ted ha dado mas de una docena de conferencias profesionales en Canada, Europa y USA, ha sido coautor de “Advisor Answers” (Respuestas Advisor), una columna para la revista FoxPro Advisor. Contacte a Ted vía telefónica (781) 663-7400, o vía email en tedroche@compuserve.com

Derechos Reservados © 1999-2003 por Ted Roche bajo licencia Creative Commons Attribution - ShareAlike 1.0 - vea http://creativecommons.org/licenses/by-sa/1.0/ para mas detalles.

1 de noviembre de 2017

Escuchar un informe

Artículo original: Listening to a report (https://msdn.microsoft.com/en-us/library/ms947682.aspx)
Autor: Doug Hennig
Traducido por: Ana María Bisbé York


Microsoft ha abierto la arquitectura del motor de informes en Visual FoxPro 9 al comunicar el motor con la clase base ReportListener. Al subclasear ReportListener, los desarrolladores VFP pueden crear sus propias salidas personalizadas. Este mes, Doug Hennig, nos presenta la nueva clase  ReportListener y nos muestra como solucionar problemas del mundo real.

Estoy seguro de que ya se ha dado cuenta, de que el área que ha recibido mayor impulso en VFP 9.0 es el sistema de  informes. Tanto el Diseñador de informes como el motor de informes (responsable de ejecutar los informes) han recibido grandes mejoras y se han dotado de características nuevas muy interesantes.

Antes de VFP 9.0, el motor de informes era monolítico: controla todo el control de los datos, posicionamiento de objetos, generación, e impresión. El nuevo motor de informe de VFP 9.0 divide la responsabilidad de la generación de informes entre el motor de informes, que controla la generación, y la salida. VFP 9 incluye ambos, el nuevo motor de informes  y el viejo, así que puede ejecutar sus informes en cualquiera de los dos motores. Microsoft se refiere al motor nuevo como salida "asistida por objetos"

Utilizar la salida asistida por objetos

Existen tres formas para decirle a VFP que emplee el nuevo motor de informes:

Instanciar un ReportListener (sea una clase base o una subclase) y especificar su nombre en la nueva cláusula OBJECT del comando REPORT o LABEL. Este es el proceder más flexible porque puede especificar exactamente qué clase listener va a utilizar; pero requiere que modifique los comandos REPORT o LABEL existentes en su aplicación.

loListener = createobject('MyReportListener')
report form MyReport object MyReportListener

Especificar el tipo de listener utilizando la cláusula OBJECTTYPE. Existen varios tipos de listeners integrados: 0 significa impresora, 1 significa presentación preliminar, 4 significa XML y 5 significa salida HTML. Puede definir y utilizar tipos personalizados

report form MyReport object type 1 && preview

Utilizar el nuevo comando SET REPORTBEHAVIOR 90 antes de ejecutar el informe, usualmente, se colocará cerca del inicio de la aplicación para que todos los informes utilicen el nuevo motor. Al especificar TO PRINTER en una cláusula del comando REPORT FORM hace que VFP utilice el objecttype = 0 que tiene integrado, de igual forma, si indica PREVIEW hace que VFP utilice el type = 1. Esta es sin duda, la opción más conveniente ante las otras mencionadas, pero no le brinda el control que necesita al instanciar su propio listener. Para restablecer el motor anterior utilice SET REPORTBEHAVIOR 80.

Al ejecutar un informe utilizando alguno de los dos últimos métodos, es llamada la aplicación especificada en la nueva variable de sistema _REPORTOUTPUT  (de forma predeterminada ReportOutput.APP en la carpeta raíz de VFP) para determinar qué clase listener se debe instanciar para el tipo especificado. Sin embargo, debido a que es una aplicación VFP, puede sustituir su propia aplicación estableciendo para ello el valor correspondiente en _REPORTOUTPUT.

Asegúrese de distribuir ReportOutput.APP (o su aplicación que la sustituye) a sus usuarios para que sus aplicaciones puedan utilizar salida asistida por objeto. Nota: En tiempo de ejecución fuera del entorno de desarrollo de VFP _REPORTOUTPUT debe ser establecida explícitamente en su código para apuntar a ReportOutput.APP o su sustituta. Lo mismo se aplica a las variables de sistema. _REPORTPREVIEW (ReportPreview.APP) y, si es necesario, en su aplicación en tiempo de ejecución _REPORTBUILDER (ReportBuilder.APP). Existen otras vías para controlar estos requerimientos, tales como colocar el código desde ReportOutput.APP en su proyecto. Un conjunto de tópicos muy útiles se pueden encontrar en el índice del archivo Ayuda de VFP bajo el título: "Report Output Application."

Dentro de ReportListener

Debido a que ReportListener es una clase base de VFP, se puede subclasear para implementar cualquier comportamiento que se desee. Antes de que pueda crear sus propios listeners, necesita entender qué propiedades, métodos y eventos (PEMs) están disponibles. Por cuestiones de espacio, voy a referirme solamente a las PEMs más importantes, vea la Ayuda de VFP para completar los detalles de todo el conjunto.

Nota de la traductora: Vea el artículo "Extender los informes en tiempo de Ejecución", donde el propio Doug Hennig detalla estas PEMs.

Una de las propiedades más importantes es ListenerType. Esta propiedad dice al report listener cómo debe ser la salida. El valor predeterminado es -1, lo que indica que no se produce salida alguna. Se configura con 0 para impresora o con 1 para salida a una ventana de presentación preliminar. Especificar 2 ó 3 produce resultados interesantes: el informe es ejecutado y las páginas se generan en la memoria; pero en realidad no se produce ninguna salida. Puede utilizar estos valores cuando desea un control sobre el tipo de salida a crear. Por ejemplo, con el ListenerType = 2, VFP genera la primera página y llama al método OutPutPage, luego genera la siguiente página y vuelve a llamar al método OutputPage, y así sucesivamente. Utilizar el 3 para ListenerType provoca que todas las páginas se generen en memoria; pero el método OutputPage no se llama automáticamente para cada página, permitiendo llamar a OutputPage para solicitar desde la memoria las páginas en el orden en que se deseen.

Antes de que se ejecute un informe, el motor de informe abre una copia del informe como un cursor de sólo lectura nombrado FRX en una sesión de datos privada. El ID de esta sesión de datos se guarda en la propiedad FRXDataSession del ReportListener. Si necesita acceder a los datos que serán mostrados en el informe, la propiedad CurrentDataSession indica qué sesión de datos utilizar.

La propiedad CommandClauses contiene una referencia a un objeto que contiene propiedades con información acerca de cómo se ejecutará el informe. Por ejemplo, la propiedad Preview = .T. si el informe se va a mostrar con vista preliminar y la propiedad OutputTo es 1 si el informe se va a imprimir.

El motor de informe dispara eventos del report listener cuando se ejecuta el informe. Existen además otros métodos disponibles que puede llamar si es necesario. Algunos de los eventos y métodos más importantes se muestran en la tabla 1

Tabla 1. Algunos de los métodos y eventos de ReportListener.

EventoDescripción
BeforeReportSe dispara antes de que se ejecuta el informe
AfterReport Se dispara después de que se ejecuta el informe
EvaluateContents Se dispara antes de que se genera un campo
AdjustObjectSize Se dispara antes que se genere una figura o forma
Render Se dispara cuando es generado un objeto
OutputPage Se llama para obtener determinada página en un dispositivo determinado.
CancelReport Se llama para cancelar el informe

EvaluateContents, AdjustObjectSize, y Render son especialmente útiles porque permiten cambiar algo acerca de los objetos antes de que se generen. Junto con otros parámetros (los veremos luego), estos eventos reciben el número de registro para el objeto actual en el cursor FRX. Puede encontrar ese registro en el cursor para determinar si el objeto debe ser generado de forma diferente a lo normal.

_ReportListener

La carpeta FFC del directorio raíz de VFP contiene una nueva biblioteca de clases en VFP 9.0

_ReportListener.VCX.  Esta biblioteca contiene varias subclases ReportListener. Puede considerar utilizar algunas de estas como punto de partida para su propias subclases ReportListener porque estas agregan funcionalidades muy útiles para su clase base.

Una de las mejoras más útiles es el soporte para encadenar varios listeners junto con el mecanismo de sucesores. Al establecer la propiedad Successor de uno de los listeners igual a una referencia a otro informe permite que ambos interactúen con el proceso de generación de informes. Esto significa que puede escribir pequeños listeners que hagan justamente una cosa y encadenar varios para obtener los diferentes efectos unidos. La propiedad IsSuccessor le dice si el listener es el primero, el líder, (es aquel con el que se comunica el motor de informes, porque es el especificado en la cláusula OBJECT de un comando REPORT o LABEL.)

_ReportListener brinda además un grupo de métodos útiles. SetFRXDataSession establece la sesión de datos del cursor FRX. SetCurrentDataSession establece la sesión de datos de los datos del informe. ResetDataSession restablece el ID de la sesión de datos a aquel en el que está el listener.

Ahora que tiene ya esta introducción, es hora de ver algunos ejemplos prácticos.

Establecer formatos dinámicamente.

Una de las primeras cosas en las que yo pensé utilizar un listener fue para establecer formatos dinámicamente. Estoy seguro de que ya han hecho esto antes: Su cliente quiere que un campo sea impreso en rojo bajo determinadas condiciones y en negro, bajo otras. Esto se podía hacer antes de VFP 9.0 creando dos copias para el mismo campo, una en rojo y otra en negro, con condiciones excluyentes en Imprimir cuando, algo como Cantidad >= 100 y Cantidad < 100 y se superponen en el mismo sitio en el informe. Esto trabaja, es cierto; pero es muy difícil de mantener, específicamente si tiene muchos de estos campos en el informe.

Con un report listener, puede cambiar el formato de un campo cuando se está ejecutando el informe en lugar de hacerlo en el Diseñador de informes. La clave para esto es el evento EvaluateContents, el que se dispara justo antes de que se genere cada campo. A este evento se pasa el número de registro del objeto actual del cursor FRX  y una referencia a un objeto que contiene propiedades con información acerca del campo. (vea la Tabla 2)

Tabla 2. Propiedades del objeto oObjPropierties pasado a EvaluateContents.

PropiedadTipoDescripción
FillAlpha N Color (alpha) o transparencia del color de relleno. Rango de valores desde 0 para transparente hasta 255 para opaco.
FillBlue N Porción azul del valor RGB() para el color de relleno
FillGreen N Porción verde del valor RGB() para el color de relleno
FillRed NPorción roja del valor RGB() para el color de relleno
FontName C Nombre de la fuente
FontSize NTamaño de la fuente
FontStyle NUn valor que representa el estilo de fuente. Los valores son: 1 (negrita), 2 (itálica), 4(subrayada) y 128 (tachada)
PenAlpha NColor del pincel (alpha)
PenBlue NPorción azul del valor RGB() para el color de pincel
PenGreen NPorción verde del valor RGB() para el color de pincel
PenRed NPorción roja del valor RGB() para el color de pincel
Reload LIguale a .T. para notificar al motor de informe que ha modificado una o más propiedades.
Text CTexto a mostrar para el objeto campo.
Value VariosValor real del campo a mostrar

DynamicFormatting.PRG, incluido en la descarga que se acompaña, define tres clases.

DynamicListener define qué es lo que debe hacer un listener dinámico y dos subclases: DynamicForeColorListener y DynamicStyleListener, cambian el color  y el estilo respectivamente, del campo que tenga una directiva en su campo memo USER. (Puede acceder al campo memo del objeto a generar desde la ficha Other (Otros) del diálogo Propiedades). La directiva en este ejemplo es una de las siguientes:

*:LISTENER FORECOLOR = ColorExpression
*:LISTENER STYLE = StyleExpression

ColorExpression  es una expresión que evalua a un valor RGB, algo como IIF(Cantidad > 50, RGB(255, 0, 0), RGB (0, 0, 0)), lo que significa utilizar Rojo si la cantidad es mayor que 50 y negro si no lo es. StyleExpression  es una expresión que evalúa un valor de estilo (vea la propiedad FontStyle  en la Tabla 2), algo como IIF(Cantidad > 50, 1, 0), lo que significa utilizar Negrita si la cantidad es mayor que 50 y normal si no lo es.

La primera tarea que debe hacer el listener es identificar qué campos tienen directivas. En lugar de hacerlo cada vez que se evalúa el campo, DynamicListener lo hace en el método BeforeReport. Selecciona la sesión de datos del cursor FRX al llamar a SetFRXDataSession, luego recorre el cursor, buscando registros con la directiva adecuada (especificado en la propiedad .cDirective) en el campo memo USER, y colocando la siguiente expresión la directiva del elemento del registro en el arreglo.

Debido a que DynamicListener hereda las características del sucesor de _ReportListener (que llama automaticamente al método BeforeReport  para todos los listeners sucesores), cada una de nuestras subclases de DynamicListener van a tener su propia matriz aRecords de los registros FRX que se correspondan con la propiedad .cDirective. Así, cada listener puede determinar fácilmente sobre qué campo debe accionar durante la ejecución del informe.

La siguiente tarea es aplicar la directiva cuando sea necesario. EvaluateContents verifica si el elemento actual de la  matriz tiene una expresión y si es así, la evalúa. Luego llama al método ApplyDirective, el que es abstracto en DynamicListener; pero implementado en sus dos subclase. Por ejemplo DynamicForeColorListener establece las propiedades adecuadas del color a toObjProperties e iguala la propiedad Reload  a .T., de tal forma que el motor de informes sabe si el formato del campo cambió.

He aquí otra tarea de mantenimiento: asegúrese de que el ListenerType está definido correctamente. El valor predeterminado, -1, provoca que no haya salida alguna, y no cambia ni siquiera especificando PREVIEW o TO PRINTER en los comandos REPORT o LABEL. Entonces, el método LoadReport establece el valor adecuado de ListenerType si es necesario.

He aquí el código para estas clases:

define class DynamicListener as _ReportListener of ;
  home() + 'ffc\_ReportListener.vcx'
dimension aRecords[1]
&& una matriz con información de cada registro en el FRX
cDirective = ''
&& la directiva que esperamos encontrar

* Si no se ha indicado ListenerType, se determina en dependencia
* de si el informe será impreso o visualizado (preview).
function LoadReport
  with This
    do case
      case .ListenerType <> -1
      case .CommandClauses.Preview
        .ListenerType = 1
      case .CommandClauses.OutputTo = 1
        .ListenerType = 0
    endcase
  endwith
  dodefault()
endfunc

* Antes de ejecutar el informe, recorremos el FRX y
* guardamos información sobre cada campo en el que esperamos 
* una directiva en su campo memo USER en la matriz aRecords.
function BeforeReport
  dodefault()
  with This
    .SetFRXDataSession()
    dimension .aRecords[reccount()]
    scan for .cDirective $ USER
      .aRecords[recno()] = strextract(USER, ;
        .cDirective + ' =', chr(13), 1, 3)
    endscan for .cDirective $ USER
    .ResetDataSession()
  endwith
endfunc

* Si el campo que se va a generar tiene una directiva, se aplica.
function EvaluateContents(tnFRXRecno, toObjProperties)
  local lcExpression, ;
    luValue
  with This
    lcExpression = .aRecords[tnFRXRecno]
    if not empty(lcExpression)
      luValue = evaluate(lcExpression)
      .ApplyDirective(tnFRXRecno, ;
        toObjProperties, luValue)
    endif not empty(lcExpression)
    * Si tenemos un sucesor, le damos participación también.
    if vartype(.Successor) = 'O'
      .Successor.EvaluateContents(tnFRXRecno, ;
        toObjProperties)
    endif vartype(.Successor) = 'O'
  endwith
endfunc

* Método abstracto para aplicar nuestra directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
endfunc

enddefine

define class DynamicForeColorListener ;
  as DynamicListener
cDirective = '*:LISTENER FORECOLOR'

* Aplicar la directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
  local lnPenRed, ;
    lnPenGreen, ;
    lnPenBlue
  if vartype(tuValue) = 'N'
    lnPenRed = bitand(tuValue, 0x0000FF)
    lnPenGreen = bitrshift(bitand(tuValue, ;
      0x00FF00), 8)
    lnPenBlue = bitrshift(bitand(tuValue, ;
      0xFF0000), 16)
    with toObjProperties
      if .PenRed <> lnPenRed or ;
        .PenGreen <> lnPenGreen or ;
        .PenBlue <> lnPenBlue
        .PenRed = lnPenRed
        .PenGreen = lnPenGreen
        .PenBlue = lnPenBlue
        .Reload = .T.
      endif .PenRed <> lnPenRed ...
    endwith
  endif vartype(tuValue) = 'N'
endfunc

enddefine

define class DynamicStyleListener as DynamicListener
  cDirective = '*:LISTENER STYLE'

* Aplicar la directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
  if vartype(tuValue) = 'N'
    toObjProperties.FontStyle = tuValue
    toObjProperties.Reload = .T.
  endif vartype(lnStyle) = 'N'
endfunc

enddefine

El programa TestDynamicFormatting.PRG muestra cómo encadenar estos listeners, de forma tal que ambos se utilicen en la generación del informe.

use _samples + 'Northwind\Orders'
loListener = newobject('DynamicForeColorListener', ;
  'DynamicFormatting.prg')
loListener.Successor = ;
  newobject('DynamicStyleListener', ;
  'DynamicFormatting.prg')
report form TestDynamicFormatting.FRX preview ;
  object loListener

La Figura 1 muestra el resultado de la ejecucón de este programa. En algunos registros, Shipped Date se muestra en rojo y en otros casos en negro. Esto se debe a que tiene la siguiente directiva en su campo memo USER

*:LISTENER FORECOLOR = iif(SHIPPEDDATE > ORDERDATE + 10, rgb(255, 0, 0), rgb(0, 0, 0))


Figura 1

El campo Ship Via aparece a veces en negrita y algunas veces normal, porque tiene la siguiente directiva en su campo memo USER:

*:LISTENER STYLE = iif(SHIPVIA = 3, 1, 0)

(Observe que aunque este campo es numérico, muestra valores como "Fedex," "UPS," o "Mail"  debido a la expresión que hay en el campo.

¿Qué más puede hacer?

Pues casi todo lo que desee. En artículos futuros, mostraré otros listeners que permitan obtener imágenes rotadas, salidas HTML con una tabla de contenidos, y otros muchos tipos de salidas.

Resumen

Microsoft superó todas las ligas de la extensibilidad en VFP 9.0 de muchas formas, incluidas en el generador de informes. Gracias a su clase base, que podemos subclasear, ReportListener permite crear una salida propia, personalizada. Háganme saber, por favor de cualquier listener interesante que ha creado o que ideas se le ocurren para hacer con listeners.

26 de octubre de 2017

Propuesta de Código Óptimo

Ahora estoy implementando una rutina, bajo un código que quiero que sea el óptimo en resultados, en tamaño y en tiempo. Expongo aquí las pautas y algunas de las limitaciones con las que me he encontrado de momento:

Supongamos que para un Conjunto Dado C tenemos N elementos, estos N elementos tienen todos por un lado un valor (podria ser un importe, p.ej) y tienen todos también un coste derivado de la relación entre C para llegar a cada undo de los elementos N.

Prentendo poder pasar unos valores CMin y CMax para los que las suma de valores de Subconjuntos del Conjunto C esten entre esos dos parámetros y de todos los posibles resultados encontrar el que me de un valor cuya suma de Costes del Subconjunto resultante sea la mínima posible de entre todas las posibilidades.

Voy a explicarlo en un ejemplo que quedará muchisimo más claro que todo este rollo raro:

Supongamos que B son Bancos y que el Conjunto C (BSCH) es el conjunto de recibos (Elementos) que deseo/puedo llevar a ese banco para descontar en N58.

Para cada Recibo N, tengo guardados 2 valores: 1º el importe del recibo, 2º el coste que me supone (aplicando los % y comisiones pactadas con el banco tratado y que tengo en una tabla de Cond. Bancarias).

CMin es un importe mínimo que yo debo llevar al banco, y del que no puedo bajar porque tengo por ejemplo, unos pagos que sé que hay que cubrir.

CMax es un importe máximo porque tengo la linea de descuento muy saturada y sólo me permite enviarles ese importe, con lo que no puedo llevar recibos que sumados superen este parametro. (con un relativo margen razonable, exacto es imposible).

Se Trata de Tratar todos Los Recibos enviados para este Banco y buscar sus posibles sumas combinatorias entre TODOS ellos y encajar ese importe entre los dos valores CMin y CMax obteniendo un Subconjunto Resultante tal que el coste de los intereses y comisiones bancarios sea el mínimo posible de todas las opciones que cumplan estos requisitos.

Notas:

Tengo desarrollados dos métodos.

  1. En método iterativo, es decir la programación habitual de todos nosotros, en general y la más rapida de pensar, pero no me acaba de encajar bien los resultados en cuanto al mejor coste posible.
  2. Lo tengo hecho por métodos de programación avanzada (y muy teoricos) por los teoremas de Dijkstra, Ford y Floyd... resumiendo... Backtraking de Seguimiento de Arboles y diagramas con el objetivo de obtener los caminos mínimos entre los distintos nodos del grafo (árbol). Este segundo la verdad es que me funciona muy bien al menos de momento, pero y parece ser el óptimo en resultados.... pero de momento me hallo en algún apurillo técnico... os lo resumo:
    1. El orden resultante de operaciones (sin optimizar demasiado)
    2. Es de 2^n és decir 2 a la n, siendo n el nº de recibos. o sea que para n = 4 recibos, 6 recibos, 8, recibos, 16 recibos y 50 recibos me da un total de operaciones:

      2^4 = 16 operaciones

      2^6 = 64 operaciones

      2^8 = 256 operaciones

      2^16 = 65536 operaciones

      2^25 = 33554432 operaciones

      2^50 = 1125899906842624 operaciones

      Quereis que os diga el problema o SALTA A LOS PUT... Ojos.... Perdonad !!!! :(

      Es decir, que sera la mar de óptimo y pijo... pero al menos por el momento me es totalmente inviable... Alguien no conoce alguna empresa que lleve más de 50 recibos a descontar N58, o da igual, 25 que ya ronda los 33 millones de operaciones ????

      Bueno, cabe decir que esto es para acojonar un poco, pq. hay mejoras relativamente fáciles de implementar que mejoran en algo el resultado, pero aún así ya aviso que no lo suficiente... Si estoy en algunas otras que creo que me lo pueden rebajar bastante y sino he pensado en la posibilidad de mezclar partes de los dos códigos (1º Iterativo y 2º Recursivo) y creo que por ahí tb. podria funcionar mejor (pq. así, ya es para dejarlo ahora mismo).

    3. Este problema... es peor que el anterior....
    4. Ehhh... continuad leiendo... pq. a ver si me equivoco y tiene fácil solución...

      Uso Técnicas de Backtracking y recursividad, eso significa llamadas dentro de la función X a la misma Función X, que pasa ??? que si no logro hallar un resultado o cortar (podar) el árbol de llamadas, llega un momento en que me salta un error de OverFlow por Anidamiento... no recuerdo cual es el máximo de llamadas anidadas permitidas en Vfp. 6.0 pero Toni Planas (Buen Compañero mío, saludos REY !!!!) me comento que le suena o cree que son 27... por lo que tengo el problema que aún solucionando lo de las combinaciones que seria resolver el tiempo de ejecución, estaria delante del problema técnico de que si no resuelvo (por bueno) o corto (por malo) el proceso de cada posibilidad antes de llegar a este "suspuesto" limite técnico... me "petará" por desborde.

La pregunta es ??? es evitable modif. algun parametro o set ??? pasa en Vfp. 8 o en Vfp. 9... y en ASP (pq. seguramente lo acabaré traduciendo) ???

Os pongo un ejemplo (con nº's totalmente aleatorios, pero para que veais el funcionamineto):

CMax = 9999999 (sin limite), pero CMin=15000

Conjunto C (Vector)   && Valores aleatorios, son sólo para pruebas !!!!

Pos  Imp   Coste
[1] 8,727 (20.40)
[2] 9,147 (22.15)
[3] 1,142 ( 6.34)
[4] 4,111 (11.67)
[5] 3,152 (35.36)
[6] 2,164 (19.09)
******************************************************************************************************************************
[1]     0 ( 0.00)+[2]     0 ( 0.00)+[3]     0 ( 0.00)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 17,874 * (42.55)
[1]     0 ( 0.00)+[2]     0 ( 0.00)+[3]     0 ( 0.00)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 21,985 * (54.22)
[1]     0 ( 0.00)+[2]     0 ( 0.00)+[3] 3,152 (35.36)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 21,026 * (77.91)
[1]     0 ( 0.00)+[2]     0 ( 0.00)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5]     0 ( 0.00)+[6] 9,147 (22.15) = 16,410 * (69.18)
[1]     0 ( 0.00)+[2]     0 ( 0.00)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6]     0 ( 0.00) = 15,990 * (67.43)
[1]     0 ( 0.00)+[2] 2,164 (19.09)+[3]     0 ( 0.00)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 20,038 * (61.64)
[1]     0 ( 0.00)+[2] 2,164 (19.09)+[3]     0 ( 0.00)+[4] 4,111 (11.67)+[5]     0 ( 0.00)+[6] 9,147 (22.15) = 15,422 * (52.91)
[1]     0 ( 0.00)+[2] 2,164 (19.09)+[3]     0 ( 0.00)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6]     0 ( 0.00) = 15,002 * (51.16)
[1]     0 ( 0.00)+[2] 2,164 (19.09)+[3] 3,152 (35.36)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 23,190 * (97.00)
[1]     0 ( 0.00)+[2] 2,164 (19.09)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5]     0 ( 0.00)+[6] 9,147 (22.15) = 18,574 * (88.27)
[1]     0 ( 0.00)+[2] 2,164 (19.09)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6]     0 ( 0.00) = 18,154 * (86.52)
[1] 1,142 ( 6.34)+[2]     0 ( 0.00)+[3]     0 ( 0.00)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 19,016 * (48.89)
[1] 1,142 ( 6.34)+[2]     0 ( 0.00)+[3]     0 ( 0.00)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 23,127 * (60.56)
[1] 1,142 ( 6.34)+[2]     0 ( 0.00)+[3] 3,152 (35.36)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 22,168 * (84.25)
[1] 1,142 ( 6.34)+[2]     0 ( 0.00)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5]     0 ( 0.00)+[6] 9,147 (22.15) = 17,552 * (75.52)
[1] 1,142 ( 6.34)+[2]     0 ( 0.00)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6]     0 ( 0.00) = 17,132 * (73.77)
[1] 1,142 ( 6.34)+[2] 2,164 (19.09)+[3]     0 ( 0.00)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 21,180 * (67.98)
[1] 1,142 ( 6.34)+[2] 2,164 (19.09)+[3]     0 ( 0.00)+[4] 4,111 (11.67)+[5]     0 ( 0.00)+[6] 9,147 (22.15) = 16,564 * (59.25)
[1] 1,142 ( 6.34)+[2] 2,164 (19.09)+[3]     0 ( 0.00)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6]     0 ( 0.00) = 16,144 * (57.50)
[1] 1,142 ( 6.34)+[2] 2,164 (19.09)+[3] 3,152 (35.36)+[4]     0 ( 0.00)+[5]     0 ( 0.00)+[6] 9,147 (22.15) = 15,605 * (82.94)
[1] 1,142 ( 6.34)+[2] 2,164 (19.09)+[3] 3,152 (35.36)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6]     0 ( 0.00) = 15,185 * (81.19)
[1] 1,142 ( 6.34)+[2] 2,164 (19.09)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5]     0 ( 0.00)+[6] 9,147 (22.15) = 19,716 * (94.61)
[1] 1,142 ( 6.34)+[2] 2,164 (19.09)+[3] 3,152 (35.36)+[4] 4,111 (11.67)+[5] 8,727 (20.40)+[6]     0 ( 0.00) = 19,296 * (92.86)
******************************************************************************************************************************
[1]     0 ( 0.00)+[2]     0 ( 0.00)+[3]     0 ( 0.00)+[4]     0 ( 0.00)+[5] 8,727 (20.40)+[6] 9,147 (22.15) = 17,874 *(42.55)

Notas: Cabe decir que en este ejemplo ya se ve una de las mejoras, pero no siempre sale así de rapido, aunque es bueno que tengamos este.

Los elementos, se deben procesar (TODOS CON TODOS, no es necesario un orden concreto, siempre que su suma supere CMIN y por debajor de CMAX (más o menos, este valor es más de referencia)

Mejoras propuestas (algunas ya implementadas, otras aún no):

  1. Ordenar los Elementos de de C por Importe ASC, de esta forma, hallando el 1er elemento que supere CMIN, podemos obtener un primer SubConjC que seran todos los valores que de por si ya superen CMin y de ellos buscar el de mín. coste con lo que obtendriamos un primer candidato a falta de procesar ahora ya con otro metodo los que han quedado por debajo de CMIN.
  2. Si al vector que nos ha quedado ahora con los elementos de C cuyo importe sea menor q CMIN y por tanto necesitaremos sumarlos con otro/s de este vector para cubrir el minimo requerido. Si ordenamos estos por Importe DESC... las primeras sumas llegaran mucho más rapido al mín. que si lo hacemos al revés... y de esta forma ya empezamos a tener unos costes mín. que nos permitiran (ver. Punto 3)
  3. Teniendo almacenado el Subconjunto que de momento nos de el coste mín. y la suma de costes de esos elementos nos dará ese valor... si procesamos otra posibilidad, y aunque no la hayamos terminado, en algun momento su mín ya supera el que nosotros tenemos como óptimo... no hace falta continuar... pq. no lo mejorariamos... ok ???

Buenos... Mentes !!!! Os pido, si teneis ganas... porque es un poco galimatias... que penseis en mejoras o si de existir en limitaciones técnicas... sabéis de formas de eviatrlas o de versiones que las amplien...

Y por dudas, ya sabeis... un mensaje y hablamos...

Grácias a todos y un fuerte abrazo.....

VIsual FoxPro ForEver !!!

Josep Mª Picañol

21 de octubre de 2017

Aplicaciones VFP a traves de Internet usando VPN

Hace algunos años, allá por la versión FoxPro 2.6 para Windows, necesitaba ejecutar una aplicación a través de Internet. Desde entonces hasta la fecha he probado un sin fin de productos. Desde FoxWeb, FoxNet, WestWind Connection, AFP, Fox#,VFPServer, VFP Framework, etc. Desde servidores construidos por el propio código VFP hasta CGI o interacción con ASP.

Primeramente, que es una aplicación VFP a través de Internet? Necesito en verdad crear una interface a través de un navegador para acceder a las bases de datos de la aplicación? Necesito controles llamativos como DBI o Voodo para una interfaz mas rica al usuario? Necesito solamente la transferencia de datos desde un lugar remoto para actualizar, capturar, consultar?

Por cierto que hay varios tipos de necesidades, desde las del usuario (interfaz, facilidad, compatibilidad) hasta las técnicas (sobrecarga del servidor, memoria de la aplicación, instalaciones en los clientes, etc.) Es decir, en algunos casos es necesario tener un servidor web configurado para traer los datos bajo demanda, rápidamente y sin tener que instalar aplicación alguna en los clientes a control remoto, basta un simple navegador para tener los datos. Pero en otros casos, quizá lo que realmente necesitamos es: CONECTAR NUESTRA APLICACION CON LOS DATOS A TRAVES DE INTERNET COMO SI LO HICIERAMOS A TRAVES DE LA RED LOCAL.

Para ello necesitamos lo siguiente:

  1. Una VPN
  2. Una aplicación VFP cualquiera que una pequeña modificación para acceder a la base de datos.
  3. Una conexión a Internet (banda ancha de preferencia)
  4. Un servidor y un cliente.

Recordemos que necesitamos un servidor donde residen nuestra base de datos, esto funciona bien para nuestra LAN, y un cliente o clientes donde tendremos nuestra aplicación. Es posible que varias PCs, laptops tengan el sistema VFP que queremos usar, así que este estará instalado completamente con todas sus pantallas e interfaces, lo único que haremos será conectarnos con el servidor de datos para trabajar con ellos.

PASOS A SEGUIR

El primer paso es usar una VPN (Virtual Private Network). Se que hay varias soluciones tanto de hardware como de software, pero aquí mismo me encontré hace tiempo una excelente: HAMACHI. Pequeña, gratuita y fácil de manejar.

Así, descarguemos Hamachi desde su pagina https://secure.logmein.com/hamachi.msi. En 10 minutos estaremos usando nuestra VPN.

Una vez descargado, necesitamos instalar el Hamachi en lo que será nuestro servidor, puesto que funciona de manera transparente a través de Firewalls, la instalación es por demás sencilla. Necesitamos crear nuestra red, y dar de alta el nombre de nuestro server, así como su password. La empresa entonces nos dará una IP especial a esta computadora. Podemos hacer que Hamachi cargue de manera automática al encender la PC Servidor (aunque si es Servidor, este nunca se apagara).

Ahora, instalamos Hamachi en la computadora cliente, siguiendo casi los mismos pasos, pero en este caso, al ya tener una red hecha, nos incorporaremos a esta, de tal manera que tanto el servidor como el cliente estén en la misma red para que puedan reconocerse. Como los grupos de trabajo de Windows.

Carguemos el Hamachi entonces para conectarnos con el Servidor.

Aquí vemos el Hamachi en acción. Para proteger la identidad se han borrado los nombres de la red y números de IPs.

En este caso, la computadora cliente esta conectada a 3 redes Hamachi distintas. 2 en la misma empresa y una externa. Vemos que solamente 1 PC esta en línea (la segunda) que es el servidor que nos queremos conectar.

Aquí suponemos que el Hamachi esta ejecutándose del lado del servidor.

Y ESO ES TODO LO QUE NECESITAMOS PARA QUE NUESTRAS APLICACIONES FUNCIONEN A TRAVES DE INTERNET. No necesitamos un IP Fija, o conocer direcciones o dominios. Ya que Hamachi ha hecho todo esto por nosotros. Lo importante aquí, es conocer el numero de IP que este le esta dando al servidor.

Ahora, veamos como hacemos la conexión de nuestra aplicación.

En la imagen vemos que para comenzar a usar la aplicación primeramente tenemos que seleccionar la empresa. En este caso tenemos 2 empresas pero se acceden de 3 maneras diferentes:

  1. De manera local (en la propia PC o laptop)
  2. De manera Red (LAN)
  3. O por Internet.

Aunque eliminare algo de código por cuestiones de seguridad, creo que tendrán la idea. Esto funciona teniendo una tabla DBF (que puede ser libre) con unos cuantos campos como nombre de la empresa, fecha de alta, etc. Pero un campo donde pondremos la dirección donde reside.

Así, en el primer caso de manera local, tendremos:

C:\sistemas\clientes\basedato\empresa\ 

Para la red, tendremos algo así:

\\servidor\e\clientes\basedato\empresa\. 

(Noten que no estoy usando un mapeo de la unidad sino una dirección de red, que se puede sustituir por su IP si se desea).

Para internet:

\\5.61.46.25\e\clientes\basedato\empresa 

Aquí notaran la primera parte que es la dirección IP de nuestro VPN Hamachi. Estamos diciéndole que acceda al servidor que esta en esta red. El único requisito es que si vamos a usar esta aplicación con datos a través de internet, es necesario primero activar la Hamachi y que este conectado, de otra manera fallara. Y listo. Una vez configurada la ruta de los archivos, abrimos los formularios, reportes, consultas y estaremos trabajando directamente con el servidor.

Esta ruta la almacenamos en una variable publica que esta presente en toda la sesión de trabajo.

Una vez terminado, cerramos la aplicación, cerramos el Hamachi (si queremos) y listo.

La ventaja de esta técnica es que disponemos de toda la riqueza visual de VFP en nuestros formularios, controles ActiveX, etc., y el código sin grandes modificaciones ya que no nos preocupamos donde reside. He trabajado con esto desde hace meses con tablas que rondan los 200MB y solamente hay un pequeño tiempo de espera la primera vez que se conecta, pero una vez abiertos los archivos, la captura, edición, borrado de los registros es extremadamente rápida, los índices se actualizan sin problema, casi no notamos que estamos trabajando a través de internet. En lo personal me ha servido para actualizar ciertos datos desde casa en el trabajo. O hacer consultas a la base de datos.

Desventajas. Por cierto que las hay. Aunque Hamachi es gratuito, tiene un limite de 16 clientes en una misma red, es decir, solo 16 PC conectadas. Si su ancho de banda es buena, y su código es eficiente, no debe tener problemas para acceder a varios clientes al mismo tiempo, (yo lo he usado con hasta 4 clientes a la vez). El cliente tiene que tener la aplicación compilada y sus runtimes.

A su favor, es que no modificamos gran cosa en el código, mas que la ruta de los archivos, no tardamos mas de 10 min en instalar y configurar Hamachi... y es gratuito... además, no solamente nos servirá para esto, ya que al tener la VPN, podremos hacer muchas mas cosas como control remoto a distancia (con VNC por ejemplo), mejor que un ftp y mas rápido, conexiones seguras y algunas futuras aplicaciones.

Por supuesto para conexiones mas robustas y con mayor cantidad de clientes en la conexión, habrá que usar otras herramientas, pero si desean hacer una conexión rápida, barata, sencilla, fácil de administrar, creo que esta opción es bastante recomendable.

Que estén bien.

Sergio Hugo Sanchez

12 de octubre de 2017

Como crear texto como un archivo de imagen con GdiPlusX

Artículo original: How to create text as image file with GdiPlusX
http://weblogs.foxite.com/vfpimaging/archive/2007/11/24/5428.aspx
Autor: Cesar Ch.
Traducido por: Luis María Guayán


Más que una vez que he visto a la gente pedir como crear imágenes que contengan algún texto. El ejemplo de abajo es realmente muy simple.

  • Crear una fuente
  • Medir el espacio que el texto necesitará
  • Crear una imagen con el tamaño necesario
  • Dibujar el texto
  • Guardar en el disco

IMPORTANTANTE

Para ejecutar se requiere VFP9 y GdiPlusX.

Por favor, asegúrese de que tiene la última versión, porque VFPPaint utiliza algunas de las funciones que se han añadido recientemente.

https://github.com/VFPX/GDIPlusX

DO LOCFILE("System.prg")

WITH _SCREEN.System.Drawing
   LOCAL lcText
   lcText = "GdiPlusX is Cool !!!"

   * Crear una fuante
   LOCAL loFont as xfcFont
   loFont = _screen.system.Drawing.Font.New("Verdana", 32, .FontStyle.BoldItalic)

   LOCAL loTmpBmp as xfcBitmap
   loTmpBmp = .Bitmap.New(1,1)

   * Recuperar el objeto gráfico
   LOCAL loTmpGfx AS xfcGraphics
   loTmpGfx = .Graphics.FromImage(loTmpBmp)

   * Medir la cadena
   * tomar el tamaño necesario para el texto
   LOCAL loSize as xfcSize
   loSize = loTmpGfx.MeasureString(lcText, loFont)

   LOCAL loNewBmp as xfcBitmap
   loNewBmp = .Bitmap.New(loSize.Ceiling)

   LOCAL loNewGfx as xfcGraphics
   loNewGfx = .Graphics.FromImage(loNewBmp)

   * Hacer el fondo amarillo
   loNewGfx.Clear(.Color.Yellow)

   * Crear un pincel sólido
   LOCAL loBrush as xfcSolidBrush
   loBrush = .SolidBrush.New(.Color.FromRGB(255,0,0)) && Rojo

   * Crear un objeto StringFormat para dibujar el texto centrado en la imagen
   LOCAL loStringFmt as xfcStringFormat
   loStringFmt = .StringFormat.New()
   loStringFmt.Alignment = .StringAlignment.Center

   * Crear un ractángulo con las medidas del Bitmap
   LOCAL loRect as xfcRectangleF
   loRect = loNewBmp.GetBounds()

   * Dibujar la cadena
   loNewGfx.DrawString(lcText, loFont, loBrush, loRect, loStringFmt)

   * Finalmente guardar la imagen
   loNewBmp.Save("c:\MyText.Png", .Imaging.ImageFormat.Png)

   * Mostrar la imagen
   RUN /N Explorer.exe c:\Mytext.Png
ENDWITH


7 de octubre de 2017

Multiselección en un Grid

Esta la saque de http://www.tek-tips.com/faqs.cfm?fid=433. Aquí la pongo traducida:

¿Cómo agregar la funcionalidad de multiselección en un Grid?
faq184-433
Posted: 29 Jan 01 (Edited 30 Sep 06)

Si desean agregar la funcionalidad multiselección a un Grid para que emule la del explorador de Windows, intenten lo siguiente:

Subclaseen un Grid en una librería de clases y agregen 4 nuevas propiedades:

  • lMultiSelect con valor .F.
  • nActiveRow con valor 0 (cero)
  • nLastRow con valor 0 (cero)
  • nRecs2Change con valor 0 (cero)

Y un nuevo método:

  • mSelectRecords()

Asegurense de que RecordSource del Grid no este indexado, lo crean con una sentencia SELECT-SQL. El RecordSource necesita un campo logico adicional, "Selected".

En el evento AfterRowColChange() pongan:

THIS.mSelectRecords()

En el método mSelectRecords() del Grid pongan:

LOCAL lcSelected,lcRecordSource

#DEFINE VK_lSHIFT 0x10 && Relocate to a header file
#DEFINE VK_lCONTROL 0x11 && Relocate to a header file

DECLARE INTEGER GetKeyState IN WIN32API INTEGER && Relocate to where WinAPI calls are declared

WITH THIS
  .nActiveRow = .ACTIVEROW && Assign value to class property
  lcSelected = .RECORDSOURCE + [.selected] && Assign value to local variable
  lcRecordSource = .RECORDSOURCE && Assign value to local variable

  DO CASE
    CASE GetKeyState(VK_lSHIFT)    < 0    ;
        OR GetKeyState(VK_lSHIFT) > 1 && Check for shift key press

      DO CASE
        CASE .nLastRow > .nActiveRow && Last recd below current recd in grid

          .nRecs2Change = .nLastRow - .nActiveRow && Calculate no of recds to change

          REPLACE (lcSelected) WITH .T. IN (lcRecordSource) && Replace current recd
          FOR i = 1 TO .nRecs2Change
            REPLACE (lcSelected) WITH .T. IN (lcRecordSource)
            SKIP IN (lcRecordSource)
          ENDF

        CASE .nLastRow < .nActiveRow && Last recd above current recd in grid

          .nRecs2Change = .nActiveRow - .nLastRow && Calculate no of recds to change
          REPLACE  (lcSelected) WITH .T. IN (lcRecordSource) && Replace current recd

          GO .nLastRow IN (lcRecordSource) && Goto the last recd
          FOR i = 1 TO .nRecs2Change
            REPLACE (lcSelected) WITH .T. IN (lcRecordSource)
            SKIP IN (lcRecordSource)
          ENDF
      ENDC

      .lMultiSelect = .T.

    CASE GetKeyState(VK_lCONTROL) < 0 ;
        OR GetKeyState(VK_lCONTROL) > 1 && Check for control key press

      REPLACE (lcSelected) WITH .T. IN (lcRecordSource)

      .lMultiSelect = .T.

    OTHERWISE && Neither shift or ctrl pressed
      DO CASE
        CASE .lMultiSelect
          REPLACE (lcSelected) WITH .F. ;
            ALL IN (lcRecordSource) && Update all recds
        CASE .nLastRow    # 0
          TRY
            GO .nLastRow IN (lcRecordSource)
          CATCH
            GO BOTTOM IN (lcRecordSource)
          ENDTRY
          REPLACE (lcSelected) WITH .F. IN (lcRecordSource)
      ENDCASE

      GO .nActiveRow IN (lcRecordSource) && Change new value
      REPLACE (lcSelected) WITH .T. IN (lcRecordSource)

      .lMultiSelect = .F.
  ENDC

  IF RECCOUNT(lcRecordSource) > 0
    DO CASE && Set colours according to OS
      CASE UPPER(OS(1)) = [WINDOWS 5.00] && Win 2K
        .SETALL([DynamicBackColor], ;
          "IIF(&lcSelected, RGB(10,36,106), RGB(255,255,255))", ;
          [Column])
      CASE UPPER(OS(1)) = [WINDOWS 5.01] && Win XP
        .SETALL([DynamicBackColor], ;
          "IIF(&lcSelected, RGB(49,106,197), RGB(255,255,255))", ;
          [Column])
    ENDCASE

    .SETALL([DynamicForeColor], ; && All OS
      "IIF(&lcSelected, RGB(255,255,255), RGB(0,0,0))", ;
      [Column])

    .nLastRow = .nActiveRow && Mark current row for next time through
  ENDIF
ENDWITH

Seran seleccionados solos registros que se cliqueen con Mayusculas o Control. Usando los cursores direccionales no seleccionara registros.

Programaticamente pueden determinar si hay selecciones multiples de registros con:

IF THISFORM.grid1.lMultiSelect
  *!* Code
ENDIF

Otra solucion mas simple es agregar un campo numerico al cursor que esta en el RecordSource del Grid. Por ejemplo mselec n(5).

En el Init del Form poner:

THISFORMSET.frm_enviaryrecibir.grd_recibidos.SETALL("dynamicbackcolor", ;
  "IIF(mselec = 1,RGB(49,106,197), RGB(255,255,255))", "Column")
THISFORMSET.frm_enviaryrecibir.grd_recibidos.SETALL("dynamicforecolor", ;
  "IIF(mselec = 1,RGB(255,255,255), RGB(0,0,0))", "Column")

Y en el Click del Grid:

LOCAL lcSelected,lcRecordSource

#DEFINE VK_lSHIFT 0x10 && Relocate to a header file
#DEFINE VK_lCONTROL 0x11 && Relocate to a header file

DECLARE INTEGER GetKeyState IN WIN32API INTEGER && Relocate to where WinAPI calls are declared
DO CASE
  CASE GetKeyState(VK_lSHIFT) < 0 OR GetKeyState(VK_lSHIFT) > 1 && Check for shift key press
    IF mselec = 0
      REPLACE mselec WITH 1
    ELSE
      REPLACE mselec WITH 0
    ENDIF

  CASE GetKeyState(VK_lCONTROL) < 0 OR GetKeyState(VK_lCONTROL) > 1 && Check for control key press
    IF mselec = 0
      REPLACE mselec WITH 1
    ELSE
      REPLACE mselec WITH 0
    ENDIF
ENDCASE

con esto cada vez que hagan Click con Mayúscula o Control presionado van a seleccionar los registros y el Grid los pinta.

Para poder hacer algo con la selección hacen:

SELEC MICURSOR
SCAN FOR mselec = 1
  *! mi codigo para la seleccion
ENDSCAN

Suerte!!!!

Carlos Caremi

4 de octubre de 2017

Regenerar Indices de las Tablas de un DBC

Quizás no sea la gran cosa, pero, a todos nos pasa que tenemos que regenerar indices, y reindexar nuestras tablas... etc etc etc.

En la empresa donde trabajo usamos este proceso cuando las sucursales reciben los DBF en las transferencias de datos, entonces los CDX están en las sucursales y solo enviamos los DBF (en un archivo comprimido, gracias a PortalFox).

Necesitamos abrir los DBF en forma exclusiva. Y este código solo tiene en cuenta indice primarios y regulares, se puede retocar para quienes necesiten.

Bueno, vamos al código... 'databaseconteiner.DBC' es el nombre del Contenedor de las Tablas...

SELECT * FROM databaseconteiner.DBC ;
  WHERE OBJECTTYPE = 'Table' ;
  INTO CURSOR TABLAS
*
SELECT TABLAS
SCAN
  TABLA1 = TABLAS.OBJECTNAME
  USE &TABLA1 IN 0 EXCLUSIVE
  SELECT &TABLA1
  WAIT WINDOWS 'Tabla ' + ALLTRIM(TABLA1) + ' (' + TRANSFORM(RECNO('TABLAS')) + ;
    '/' + TRANSFORM(RECCOUNT('TABLAS')) + ')' NOWAIT
  FOR I=1 TO TAGCOUNT()
    IF !EMPTY(TAG(I))
      INDICE = SYS(14,I)
      NOMBRE = TAG(I)
      PRINCI = PRIMARY(I)
      IF PRINCI
        ALTER TABLE &TABLA1 DROP PRIMARY KEY
        ALTER TABLE &TABLA1 ADD PRIMARY KEY &INDICE TAG &NOMBRE
      ELSE
        INDEX ON &INDICE TAG &NOMBRE ADDITIVE
      ENDIF
    ELSE
      REINDEX
      EXIT
    ENDIF
    REINDEX
  ENDFOR
  PACK
ENDSCAN

Guillermo Gastón Giménez

1 de octubre de 2017

El alcance de CREATEOBJECT

Artículo original: The scope of CREATEOBJECT
http://www.foxpert.com/KnowlBits_200701_3.htm
Autor: Christof Wollenhaupt
Traducido por: Ana María Bisbé York


En ocasiones, uno llega a entender que algunas cosas son absolutamente desaprovechadas. Una de ellas es el alcance de CREATEOBJECT(). La primera vez que usted instancia un objeto desde una clase, Visual FoxPro crea una plantilla del objeto con todas los valores de las propiedades definidas en la clase. Si la definición de clases contiene una expresión para alguna de las propiedades, Visual FoxPro la evalúa en ese momento. Para futuras instancias, Visual FoxPro utiliza el valor de la expresión evaluada anteriormente.

Ahora, la parte desaprovechada de este artículo: Todas las propiedades de clases son evaluadas en el ámbito del procedimiento o método que instancia el objeto. Esto significa, que en las expresiones de propiedades de clases puede acceder a todas las variables locales en ese procedimiento:

Local lcVar, loRef
lcVar = "Hi"
loRef = CreateObject("Test")
? loRef.cTest

Define Class Test as Custom
 cTest = m.lcVar
EndDefine

27 de septiembre de 2017

¿Por qué son importantes las interfaces?

Artículo original: Why interfaces are important?
http://www.eps-cs.com/VFPConversion/ConversionBlog.aspx?messageid=90b096b1-65db-48c5-b7d5-e6fe2dd54f9f
Autor: Markus Egger
Traducido por: Ana María Bisbé York


.NET soporta el concepto de las interfaces junto con las clases. Eso también lo hace VFP. Sin embargo, las interfaces VFP se requieren solamente en escenarios especiales, mientras que en .NET se utilizan todo el tiempo. Esto es algo que los desarrolladores VFP necesitan saber.

Pero, ¿porqué exactamente es así? Existe un grupo de razones debido las cuales las interfaces son increíblemente importantes y útiles. Este comentario representa sólo un escenario de ejemplo. Antes de verlo, necesitamos preguntarnos "¿qué es exactamente una interfaz?" Una interfaz es la definición de métodos y propiedades que tiene un objeto. En VFP, al crear un objeto, terminará teniendo todos los métodos y propiedades definidos (lo que también es cierto para .NET). Sin embargo, con una definición específica de interfaz, es posible configurar lo que se espera. Esto es muy importante para la reutilización del código y programación genérica.

Vamos a examinar un ejemplo sencillo de VFP: Digamos que deseo crear una barra de herramientas que tenga un botón Guardar. Siempre que se oprima el botón Guardar, va a invocar al método Save() en el formulario activo. Esto significa que yo espero que el formulario tenga un método Save(). En otras palabras: Espero que el formulario "implemente una interfaz que tenga el método Save()". En VFP, puedo llamar al método Save() de esta forma:

_Screen.ActiveForm.Save()

En VFP, no tenemos que especificar que el formulario activo implemente una interfaz específica. Si un formulario tiene un método Save(), se invocará. Esto hace las cosas muy sencillas; pero tiene también un lado malo: si el formulario activo no tiene método Save(), (puede ser que el desarrollador se olvidó de agregarlo), entonces, ocurrirá un error durante la ejecución. El compilador no tiene forma de verificar si  el objeto ActiveForm tiene o no un método Save()

La escritura fuertemente tipada de .NET soluciona este problema. En .NET, las referencias de objeto siempre son de un tipo específico (o clase...lo que es lo mismo que un tipo en .NET). Entonces, digamos que tenemos una referencia a un formulario activo llamada "ActiveForm". Por ahora, no nos preocupa como ocurre). Esta referencia debería no sólo ser tipada como un objeto (como en VFP), sino que debería ser del tipo "System.Windows.Forms.Form". En VB.NET, lo podríamos definir así:

Dim ActiveForm As System.Windows.Forms.Form
ActiveForm.Save() ' Falla!!!

Sin embargo, la clase "Form" no tiene el método Save(). Por tanto, el compilador no nos permitirá seguir adelante con la definición de una referencia como "Form" y una llamada al método Save(). En su lugar, deberíamos crear una clase form especial que va a tener un método Save():

Public Class SaveForm
  Inherits System.Windows.Forms.Form
  Public Function Save() As Boolean
    ' Guarda y devuelve el código correcto en las subclases
    Return False
  End Function
End Class

Ahora, podemos utilizar este formulario para definir el tipo de nuestra variable, y entonces seremos capaces de llamar al método Save(), ya que el compilador puede verificar que cada referencia de objeto puede guardarse en esta variable puede tener un método Save():

Dim ActiveForm As SaveForm
ActiveForm.Save() ' Funciona !!!

Hasta aquí vamos bien. El problema con esto es que la herencia es una técnica muy estática. Al igual que en VFP,  podemos subclasear sólo una clase desde la clase padre en .NET. Y en este escenario, significa que estamos ya al límite de nuestras opciones. Entonces, si tenemos alguna clase muy buena, que queramos utilizar como clase padre, no podemos, porque necesitaremos heredar siempre de "SaveForm". Una vez más en  VFP no es problema, porque podemos agregar el método Save() a nuestro formulario nuevo. Pero, nuevamente podríamos tener un problema de calidad no acorde con el desarrollo moderno. Y he aquí donde las interfaces juegan su papel.

Las interfaces nos permiten definir que determinados objetos tienen determinados métodos, sin desaprovechar la relación heredada. Así que en lugar de crear una clase, podemos definir una interfaz.

Public Interface ISaveForm
  Function Save() As Boolean
End Class

Ahora, podemos crear un formulario que subclase de otra clase formulario que deseemos, y además, siempre vamos a implementar la interfaz que acabamos de definir:

Public Class MyCoolForm
  Inherits SomeOtherForm
  Implements ISaveForm
  Public Function Save() As Boolean
    ' Guardar...
    Return True
  End Function
End Class

Cuando hablamos ahora de un objeto creado a partir de esta clase, podemos verlo en formas diferentes: Ahora lo vemos como una forma genérica (sin el método Save()), o podemos verlo como el tipo "MyCoolForm" (los que tienen un método Save(); pero nuestro botón de la barra de herramientas podría no saber que desde que no tiene el concepto de que "MyCoolForm" es), o como tipo "ISaveForm" (la que tiene un método Save() y la barra de herramientas puede ser utilizado). Entonces, podemos hacer esto:

Dim ActiveForm As ISaveForm ' Utiliza la Interfaz!!!
ActiveForm.Save() ' Funciona, no importa que sea un formulario subclaseado 

Algo agradable es que es muy fácil hacerlo (una vez que estemos confortables con este concepto), y es mucho más sencillo de controlar, una vez que el compilador puede verificar 100 %, y nuestro código va a trabajar en tiempo de ejecución.

23 de septiembre de 2017

Crear una funcionalidad Deshacer en cuadros de texto de Visual FoxPro

Artículo original: Creating Undo functionality in a Visual FoxPro TextBox
http://west-wind.com/weblog/posts/3296.aspx
Autor: Rick Strahl
Traducido por: Ana María Bisbé York


El cuadro de texto de Visual FoxPro no es precisamente un gran control como el que yo tengo en el Help Builder (http://www.west-wind.com/wwHelp/). Tuve que trabajarlo para hacer que funcione como si estuviera basado en un editor de textos que incluye formato. Pero al mismo tiempo no era capaz de encontrar una forma decente de sustitución. La mayoría de los controles ActiveX basados en textos son poco menos que un infierno (al menos en VFP) o son muy muy lentos si trata de enganchar algún evento COM a la clave al procesar, como necesito que haga el Help Builder.

En general el TextBox de VFP funciona bien, salvo en dos cosas:

  1. Existe un bug en el control que provoca que el control envuelva al original si existen avance de línea en un área específica del margen derecho. Puede causar avances de línea que sean "comidos" por el cuadro de texto con el texto que puede ser un salto súbito cuando el cuadro de texto es redimensionado u otros cuadros de texto entren  fuera del margen. Esto se puede ver como un bug muy oscuro; pero si trabaja modificando grandes cantidades de textos se lo encontrará muy pronto. Según Calvin Hsia se corrige en el VFP 9.0 SP1...
  2. Comportamiento para Deshacer. El cuadro de texto de Visual FoxPro no tiene un comportamiento para Deshacer - el buffer para Deshacer se pierde cada vez que hay cualquier tipo de actualización del dato. Esto incluye el enlace al origen de datos (ControlSource), establecer el valor explícitamente, cambiar SelText o incluso pasar texto al control. Se limpia también si se oprime la tecla Tab y sale del control e inmediatamente regresa. Todo esto es realmente limitado y no es un comportamiento estándar.

Hasta la salida del SP1 no puedo hacer nada con el punto 1; pero he pensado que puedo controlar mi propio Deshacer con buffer en mi control TextBox personalizado. Mientras hablaba con Calvin en SoutWest Fox comenzamos a colocar un mejor comportamiento en el TextBox; pero sobre el comportamiento Deshacer está profundamente dentro del runtime de VFP y cambiar eso es algo como romper mucho del código que ya está. Por tanto no hay ayuda en este sentido. Entonces Calvin me sugirió... escribe el tuyo propio....

Lo primero que pensé - sí, bien. Controlar Deshacer buffer con código Fox es muy lento y ocuparía mucha memoria, porque hay que guardar el buffer entero del valor del control ya que los eventos InteractiveChange y ProgrammaticChange no brindan información de qué es lo que ha cambiado, entonces, no hay una forma fácil de capturar cuál de los cambios es el que hay que deshacer.

Después de pensarlo un poco, intenté de todas formas para observar cómo van a trabajar las cosas y entonces trabajé con estas variantes:

  • Comportamiento opcional para Deshacer personalizado
  • Deshacer que sobreviva a cambios por programa
  • Deshacer que sobreviva al foco de otro control
  • Deshacer que se limpie sólo con Refresh o un Clear explícito del buffer de Deshacer
  • Rehacer que permita Deshacer lo deshecho

He aquí un código que se encarga de un control que controla un comportamiento Deshacer en mi clase TextBox.

DEFINE CLASS wwhtmleditbox AS editbox
  OLEDropMode = 1
  FontName = "Tahoma"
  FontSize = 8
  Alignment = 0
  AllowTabs = .T.
  Height = 188
  ScrollBars = 2
  Width = 443
  oundobuffer = .NULL.
  *-- La última vez, UndoBuffer fue actualizado  en segundos. 
  *-- Internamente el valor utilizado mantiene cada carácter 
  *-- por tenerlo añadido al buffer de deshacer. 
  nlastundoupdate = 0
  *-- El indicador utilizado inhabilita los cambios de programación
  *-- en el bufer de deshacer.
  lundonoprogrammaticchange = .F.
  lundotracking = .F.
  oredobuffer = .NULL.
  Name = "wwhtmleditbox"

  PROCEDURE Init
    this.oUndoBuffer = CREATEOBJECT("wwNameValueCollection")
    this.oRedoBuffer = CREATEOBJECT("wwNameValueCollection")
  ENDPROC

  PROCEDURE undo
    IF THIS.lundotracking AND THIS.oUndoBuffer.Count > 0
      THIS.lUndoNoProgrammaticChange = .T.
      this.oRedoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
      lcValue = this.oUndoBuffer.aItems[this.oUndoBuffer.Count,2] 
      IF lcValue = this.Value AND this.oUndoBuffer.Count > 1
        THIS.Value = this.oUndoBuffer.aItems[this.oUndoBuffer.Count-1,2] 
        this.oUndoBuffer.Remove(this.oUndoBuffer.Count)
      ELSE
        this.Value = lcValue
      ENDIF
      this.SelStart = VAL(this.oUndoBuffer.aItems[this.oUndoBuffer.Count,1])
      THIS.lUndoNoProgrammaticChange = .F.
      this.oUndoBuffer.Remove(this.oUndoBuffer.Count)
    ENDIF
  ENDPROC

  PROCEDURE redo
    IF THIS.lundotracking AND THIS.oRedoBuffer.Count > 0
      THIS.lUndoNoProgrammaticChange = .T.
      this.Value = this.oRedoBuffer.aItems[this.oReDoBuffer.Count,2]
      this.SelStart = VAL(this.oRedoBuffer.aItems[this.oRedoBuffer.Count,1])
      THIS.lUndoNoProgrammaticChange = .F.
      this.oRedoBuffer.Remove(this.oRedoBuffer.Count)
    ENDIF
  ENDPROC

  PROCEDURE KeyPress
    LPARAMETERS nKeyCode, nShiftAltCtrl
    *** No desea oprimir ESC para eliminar el contenido del campo.
    IF nKeyCode = 27
      *** Se come la tecla, la ignora
      NODEFAULT
    ENDIF
    *** Ctrl-Z
    IF THIS.lUndoTracking 
      IF nKeyCode = 26
        *** Debe verificar la tecla Ctrl-<- la que tiene número 26
        DECLARE INTEGER GetKeyState IN WIN32API INTEGER
        IF GetKeyState(0x25) > -1
          THIS.Undo()
          NODEFAULT
        ENDIF
      ENDIF
      *** Rehacer Ctrl-R
      IF nKeyCode = 18
        THIS.Redo() 
        NODEFAULT
      ENDIF
    ENDIF 
  ENDPROC

  PROCEDURE ProgrammaticChange
    IF THIS.lUndoTracking AND !THIS.lUndoNoProgrammaticChange
      this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
      this.oRedoBuffer.Clear()
    ENDIF 
  ENDPROC

  PROCEDURE InteractiveChange
    IF THIS.lUndoTracking
      *** Actualizar sólo en la segunda mitad del intervalo, 
      *** entonces, si escribe varias letras va en lote 
      IF SECONDS() - THIS.nLastUndoUpdate < 1
        this.nLastUndoUpdate = SECONDS()
        RETURN
      ENDIF
      *** Solo deshace la escritura de la última palabra
      IF LASTKEY() = 32 OR LASTKEY() = 13 OR LASTKEY() = 44 OR ;
        LASTKEY() = 46 OR LASTKEY() = 9
        this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
        this.oRedoBuffer.Clear()
        this.nLastUndoUpdate = SECONDS()
      ENDIF
    ENDIF
  ENDPROC

  PROCEDURE Refresh
    IF THIS.lUndoTracking
      THIS.oUndobuffer.Clear()
    ENDIF
  ENDPROC
ENDDEFINE

Vea que este código tiene una dependencia que no incluyo aquí. Estoy utilizando una clase de usuario llamada NameValueCollection la que guarda el nombre y el valor en una matriz. Puede cambiar este código para utilizar una Collection y un objeto que guarde el valor del buffer en la posición SelStart.

La idea es esencialmente que cada InteractiveChange y ProgrammaticChange son monitoreados y potencialmente escritos fuera del valor de la colección UndoBuffer. El método InteractiveChange se  alterna de tal forma que solo escribe fuera el dato si el usuario en realidad no lo está escribiendo activamente y si el cursor está en el límite de una palabra. Esto reduce tremendamente la cantidad de valores que se guardan. Parece  que otras aplicaciones como Word utilizan un proceder similar aunque el comportamiento de Word es algo diferente.

Vea además este código

IF nKeyCode = 26
  *** Debe verificar Ctrl-<- el cual es  26
  DECLARE INTEGER GetKeyState IN WIN32API INTEGER
  IF GetKeyState(0x25) > -1
    THIS.Undo()
    NODEFAULT
  ENDIF
ENDIF

En su infinita sabiduría alguien decidió que el mapa de código de teclas (KeyCode) fuera 26 para Ctrl-Z y para Ctrl-Flecha izquierda, por lo que no hay una forma sencilla de decir el carácter. En su lugar, tiene que hacer otra verificación sobre el KeyCode para ver si tiene una Flecha izquierda (0X25). Si devuelve -127 ó -128. Diga que esto es HACK; pero funciona. Fue simpático por unos minutos tener Ctrl+Flecha izquierda atado a la tecla Deshacer (Retroceso). Menos mal que decidí generar un comportamiento de Rehacer desde el inicio...

Ahora he colocado esto en el Help Builder y como yo trabajo con los documentos del Web Connection 5.0 (http://www.west-wind.com/wconnect/) utilizo mucho características con tópicos muy largos. Entonces, este comportamiento se ve muy bien. No he visto problemas de rendimiento ni por la memoria ni nada apreciable mientras escribo.

Puedo imaginar que puedo marcar este en mi lista de deseos para VFP que nunca iba a tener.


19 de septiembre de 2017

El dilema de controlar una excepción en Visual FoxPro

Artículo original: Exception Handling Dilemma in VFP
http://west-wind.com/weblog/posts/4538.aspx
Autor: Rick Strahl
Traducido por: Ana María Bisbé York


Me ha llamado la atención, hace un par de días, que cambiando el mecanismo de control de errores de Web Connection por uno nuevo basado en TRY/CATCH, en lugar del método tradicional de controlar el error, que tiene mucha menos funcionalidad.

En Web Connection 4.0 todas las clases para procesar errores se controlan en el método Error en la clase Process la que básicamente captura todos los errores no controlados. El problema con el método de error es que no puede inhabilitarlo fácilmente, entonces, para tener un entorno de desarrollo donde los errores no sean controlados y el entorno de ejecución donde están tengo que utilizar un código como este:

*** Este bloque anula el método Error para que pueda tener los errores en
*** tiempo de depuración interactiva mientras ejecuta dentro de VFP.
#IF !DEBUGMODE
**************************************
FUNCTION Error(nError, cMethod, nLine)
**************************************
  LOCAL lcLogString, llOldLogging, lcOldError
  *** ¿Hemos sobrecargado la pila de llamadas? Si es así, nos vamos
  IF PROGLEVEL() > 125
    RETURN TO PROCESSHIT
  ENDIF
  nError = IIF(VARTYPE(nError) = "N", nError, 0)
  cMethod = IIF(VARTYPE(cMethod) = "C", cMethod, "")
  nLine = IIF(VARTYPE(nLine) = "N", nLine, 0)
  *** Guardamos el valor actual
  lcOldError = ON("ERROR")
  ON ERROR *
  *** Cerramos esta petición con una página de error 
  *** - SAFE MESSAGE (no confiar en ningún objeto)
  This.ErrorMsg("Ha ocurrido un error..",;
    "En este momento no se puede servir esta petición " + ;
    "debido a dificultades técnicas.<P>" + ;
    "No. Error: " + STR(nError) + "<BR>" + CRLF + ;
    "Error: " + MESSAGE()+ "<P>" + CRLF + ;
    "Método: " + cMethod + "<BR>" + CRLF + ;
    "Código actual: "+ MESSAGE(1) + "<BR>" + CRLF + ;
    "Línea de código actual: " + STR(nLine) + "<p>" + ;
    "Excepción controlada por " + This.CLASS + ".Error()")
  *** Obriga a cerrar el archivo y es recuperable por wc.dll/exe
  *** NOTA: Los objetos HTML no se liberan aquí debido a otra
  *** referencia de objeto como Response.
  IF TYPE("This.oResponse") = "O" AND !ISNULL(This.oResponse)
    This.oResponse.DESTROY()
  ENDIF
  * wait window MESSAGE() + CRLF + MESSAGE(1) + ;
  *   CRLF + "Método: " + cMethod nowait
  IF TYPE("This.oServer") = "O" AND !ISNULL(This.oServer)
    *** Intenta obtener un log del error - Fuerza el log!!!
    * llOldLogging = This.oServer.GetLogToFile()
    llOldLogging = This.oServer.lLogToFile
    lcLogString = "Procesando Error - " + ;
      This.oServer.oRequest.GetCurrentUrl() + ;
      CRLF + CRLF + "<PRE>" + CRLF + ;
      " Error: " + STR(nError) + CRLF + ;
      " Mensaje: " + MESSAGE() + CRLF + ;
      " Código: " + MESSAGE(2) + CRLF + ;
      " Programa: " + cMethod + CRLF + ;
      " Línea No: " + STR(nLine) + CRLF + ;
      " Cliente: " + This.oRequest.GetIpAddress() + CRLF + ;
      "Post Buffer: " + This.oRequest.cFormVars + CRLF + ;
      "</PRE>" + CRLF + ;
      "Excepción controlada por: " + This.CLASS+".Error()"
    This.oServer.LogRequest(lcLogString,"Local",0,.T.)
    This.oServer.SetLogging(llOldLogging)
    This.SendErrorEmail("Web Connection Error - " + ;
      This.oServer.oRequest.GetCurrentUrl(), lcLogString)
  ENDIF
  ON ERROR &lcOldError
  *** Regresa al método Process!
  RETURN TO ROUTEREQUEST
ENDFUNC
* EOF wwProcess::Error
#ENDIF 

Ahora está trabajando bien; pero siempre ha sido un esquema que huele mal. El primer punto es que existe un requerimiento del compilador para habilitar y deshabilitar el interruptor para el modo debug. Entonces, necesita ser recompilado para hacer que cambie entre los dos.

El otro problema más delicado, es que confía en RETURN TO para devolver a un método especificado que lo haya llamado, lo que no es siempre confiable. De hecho, si en algún lugar en la cadena de llamada causa error una ejecución EVAL() el mecanismo de error entero se rompe porque VFP 8 y 9 no admiten RETURN TO en llamadas EVALUATE(). Cuando este es el caso, RETURN TO hace return sencillo y usted termina la ejecución del código SIGUIENDO el error.

Nunca me había gustado el método Error en estos casos porque no existe una forma determinista para devolverlo a algún lugar, entonces VFP 8 llegó con algo que me gustó mucho ver: TRY/CATCH y la capacidad de tener más control determinista del error que permite volver a un lugar específico del código.

Entonces, con Web Connection 5.0 el método Error y el proceder DEBUGMODE (el que de paso, es aplicado también a otras clases) fue sustituido por un controlador TRY/CATCH como centro del motor de proceso. He aquí la implementación que no tiene ningún método de error especial; pero en su lugar, el lo llama:

************************
FUNCTION Process()
************************
  LOCAL loException
  PRIVATE Response, REQUEST, Server, Session, Process
  Process = THIS
  Server = This.oServer
  Request = This.oRequest
  Response = This.oResponse
  Session = This.oSession
  Config = This.oConfig
  *** Método gancho
  IF !This.OnProcessInit()
    RETURN
  ENDIF
  IF Server.lDebugMode
    This.RouteRequest()
  ELSE
    TRY
      This.RouteRequest()
    CATCH TO loException
      This.OnError(loException)
    ENDTRY
  ENDIF
  This.OnProcessComplete()
  RETURN .T.
ENDFUNC
* EOF wwProcess::Process 

El controlador TRY/CATCH captura cualquier error y entonces, sencillamente llama a un método sobreescribible que es utilizado para controlar el error. Un controlador predeterminado es proporcionado o el desarrollador puede sustituir OnError para hacer cualquier cosa que necesite. Desde la perspectiva del diseñador esto está mucho más claro y no tiene un error potencial utilizando RETURN TO.

Pero, existe desafortunadamente, una funcionalidad perdida. TRY/CATCH está bien; pero su empleo termina con algunas limitaciones. Este controlador:

  • No nos brinda una información detallada del error
  • No tenemos la pila de llamada - el control se devuelve al nivel que lo llama.

CATCH permite recibir un objeto exception; pero desafortunadamente este objeto no brinda mucha información y la información disponible no está completa como en el método Error. La razón principal para esto es que cuando ocurre una operación CATCH limpia la pila de llamada con lo que lo coloca en el método inicial que ha realizado la llamada. Esto significa que puede obtener información detallada del error desde el nivel de la pila de llamadas donde, realmente ocurra el error. Cualquier variable, PRIVATE o LOCAL se habrá perdido y ASTACKINFO() solamente devolverá la pila actual, la que en realidad no se corresponde con callstack de el lugar donde ocurrió el error.

Esto significa además, que su información de error está limitada por lo que provee el objeto Exception y no es tan completa como lo que brinda LINENO(), PROCEDURE y SYS(16). El resultado es que en muchas situaciones puede obtener LineNo y ejecutar el nombre del programa de la forma en que tiene en el método Error, especialmente en tiempo de ejecución sin tener información de depuración.

Alternativas? Realmente no ...

Entonces, estuve intentando obtener algunas soluciones para esta limitación - desafortunadamente no he encontrado la forma correcta de hacerlo. Mi primera idea fue hacer que funcione el método Error además del controlador TRY/CATCH. Un par de ideas que vinieron a mi mente:

  • Sobreescribir Try/Catch con el método Error
  • Utilizar un método Error junto al Try/Catch y emplear THROW para crear una excepción personalizada
FUNCTION Error(lnerror,lcMethod,lnLine)
  *** Aquí su controlador de error personalizado
  RETURN TO RouteRequest
ENDFUNC

Desafortunadamente esto no va a funcionar: VFP no permitirá RETURN TO ni RETRY dentro de un bloque TRY/CATCH. Denegado.

La segunda opción tendría un proceder similar; pero en su lugar de devuelve la información del error capturado y luego crea una excepción personalizada que contenga información adicional:

FUNCTION Error(lnerror,lcMethod,lnLine)
  This.StandardPage("Error Ocurrido","Prueba")
  LOCAL loException as Exception
  loException = CREATEOBJECT("Exception")
  loException.LineNo = lnLine
  loException.ErrorNo = lnError
  loException.Message = MESSAGE()
  THROW loException
ENDFUNC

Desafortunadamente, esto tampoco funciona el error arrojado en el método Error es arrojado a su vez, al nuevo controlador de errores. Lo único que puede controlar una excepción en el método Error es ON ERROR. Por tanto, esto tampoco funciona.

Próxima idea: Capturar la excepción y adjuntarla en una propiedad, para que la información adicional esté disponible. Entonces, sencillamente devolvemos el Error del método Error. Desafortunadamente esto tampoco funciona, porque no se puede acortar el circuito de procesamiento del error - Cualquier código siguiente al error original continúa ejecutándose.

Entonces, al final está claro que el método Error() que necesita mostrar la cadena de error no coexiste de manera limpia con el controlador TRY/CATCH. No veo al forma que puedo hacer que esto funcione utilizando un proceder donde se empleen ambos .

Al final, mi solución es agregar un indicador lUseErrorMethodErrorHandling a la clase que procesa y luego, basarme en el salto de la llamada TRY/CATCH

IF Server.lDebugMode OR ;
  This.lUseErrorMethodErrorHandling
  This.RouteRequest()
ELSE
  TRY
    This.RouteRequest()
  CATCH TO loException
    This.OnError(loException)
  ENDTRY
ENDIF

Entonces, el controlador de error funciona como en la versión vieja de Web Connection, excepto en que el desarrollador es responsable por hacer su propio controlador de error. Para simular un comportamiento similar y para componer el mensaje de error como TRY/CATCH puede hacer lo siguiente:

FUNCTION Error(lnerror,lcMethod,lnLine)
  LOCAL loException as Exception
  loException = CREATEOBJECT("Exception")
  loException.LineNo = lnLine
  loException.LineContents = MESSAGE(1)
  loException.ErrorNo = lnError
  loException.Message = MESSAGE()
  loException.Procedure = SYS(16)
  This.OnError(loException)
  RETURN TO RouteRequest
ENDFUNC

Esto funciona; pero no es lo que yo considero la implementación más limpia. Sería muy bueno que VFP nos permitiera alguna opción para tener un gancho que pueda controlarse cuando es creado un objeto Exception. En lugar de pasar una referencia a un objeto Exception podría pasar un tipo - VFP podría instanciar el tipo y usted podría sobreescribir, digamos el Init() de ese tipo para reunir toda la información relevante de la pila de llamadas hasta ese punto. O tener la referencia que tenga un método que sea llamado en la inicialización. O al menos, si TRY/CATCH admite THROW para encadenar el método error.

Bueno, esto funciona aunque tiene truco...