28 de junio de 2017

Múltiples bandas de detalle en informes (2)

La necesidad de obtener informes con múltiples bandas de detalle en informes de Visual FoxPro, es tan antigua como la propia herramienta. Pero hasta ahora no contábamos con facilidades para lograr el resultado esperado por los usuarios sin que nos representara un dolor de cabeza.

Hace unos días, escribí sobre este tema. (Múltiples bandas de detalle en informes (1)). Luego de haber adquirido un poco más de experiencia, me decido a revisar un poco el tema, para complementar algunos de sus aspectos.

¿Qué ocurría antes de VFP 9.0?

Pues, lo podíamos lograr. Pero no era nativo y había que recurrir a procesos que si bien, eran posibles, también eran complejos. Se trataba, en pocas palabras, de crear un cursor con los campos necesarios a mostrar en cada banda y algún o algunos campos adicionales, que controlaban que los datos fueran agrupados correctamente en bandas.

Dentro del Diseñador de informes, la tarea consistía en crear dentro de la única banda de detalle existente, tantos grupos de campos a mostrar como bandas finales necesitamos. En este caso, al ver el diseñador, teníamos campos superpuestos y resultaba bien difícil, como ya dije, era complejo; pero era posible. Felizmente ahora este proceder es nativo.

Múltiples bandas de detalle

Contar con la posibilidad de crear múltiples bandas de detalle es una de las mejoras más importantes y más solicitadas. A partir de VFP 9.0 es posible procesar múltiples tablas hijas para cada registro de la tabla padre. Existen posibilidades ilimitadas de lo que se puede hacer con esta nueva característica. Veamos como se controla el procesamiento de registros.

Procesamiento de registros

El informe tiene un cursor conductor - este cursor se procesa una única vez. El procesamiento de sus registros se pausa por los cambios diseñados en los grupos. Entonces el motor de informes deja de moverse por el cursor conductor y realiza las acciones pertinentes, ejemplo escribe el pie de grupo para el grupo que termina y el encabezado de grupo para el siguiente grupo y luego continúa procesando el cursor. Se admiten encabezados y pies para bandas de detalle. Son similares a los encabezados y pies de grupos en alguna medida, tienen algunas diferencias.

Para cada registro padre que se procesa, ocurre lo siguiente:

  • Se procesa el encabezado de la banda 1.
  • Se procesa el detalle de la banda 1 una vez para cada registro hijo en el alias destino asociado.
  • Se procesa el pie de la banda 1.
  • Se procesa el encabezado de la banda n
  • Se procesa el detalle de la banda n una vez para cada registro hijo en el alias destino asociado.
  • Se procesa el pie de la banda n.

Donde,

Expresión del Alias destino - es el término utilizado para describir qué tabla es la tabla conductora para una banda de detalle en particular.

Definir multibandas - De forma predeterminada los informes se crean con una sola banda de detalle. Se pueden adicionar bandas desde el diálogo Bandas Opcionales incrementando el valor dentro del control Spinner (hasta 20). Luego, hay que definir el alias para cada banda, este aspecto lo veremos con detenimiento. Al aumentar un número aparece una nueva banda de detalle y al disminuir uno se elimina la banda con mayor número (la última en haber sido creada)

El trabajo multibandas nos ofrece nuevas posibilidades; pero nos exige ser mucho más cuidadosos, porque ahora en cada banda no estará la tabla padre sino una de las hijas, y hay que definir el nombre del alias en el cuadro de texto Expresión del Alias destino (Target alias expresion,) del Cuadro de diálogo Propiedades de la banda de detalle (Detail band propierties). En caso de estar trabajando con una sola banda, pues no pasa nada si la dejamos en blanco, toma la tabla padre que será justo lo que deseamos.

El cuadro de diálogo Detalles contiene las propiedades de la banda de Detalles.

Alias destino (Target alias) - Es el alias que se recorre en la banda donde se encuentra. Es una expresión. Debe ir entre comillas, para que evalúe la expresión y obtenga los valores existentes en el Alias indicado. Si no se ponen las comillas, se entiende que es una expresión, por ejemplo Clientes, pudiendo ser una variable, o un campo que contiene el nombre del alias que se obtiene en tiempo de ejecución. Este nombre va a aparecer en la barra gris que define la banda. Se recomienda definir los campos incluyendo el nombre del alias para evitar confusiones. El tipo de alias dependerá del contenido que se quiera mostrar.

Nueva función para cláusula SUMMARY - Elimina toda la información de las bandas, incluyendo su detalle, encabezado y pie de banda. Si existen expresiones a evaluar en al entrar y al salir de alguna banda no se van a ejecutar con la cláusula SUMMARY. Por otra parte no elimina los encabezados y pies de grupos que se hayan definido y sus expresiones a evaluar para en al entrar y al salir del grupo sí se van a evaluar.

Las variables y expresiones calculadas en el informe adquieren una nueva dimensión, Reset based on, en la ficha Variables de la ventana Propiedades indica que se deben procesar los registros de la tabla hija y no se limpia hasta que no termine el informe, lo que permite realizar cálculos y combinaciones entre las bandas utilizando valores calculados de unas en otras. Al seleccionar un número de banda específica, VFP procesa el cálculo solamente durante el proceso de esa banda. Si se escoge más de un sitio, pues se procesa mientras duren las bandas seleccionadas.

Diferencias entre las Bandas de Detalles y las Agrupaciones de Datos (Data Grouping)

Sirven para mostrar diferentes tipos de relaciones entre los datos. Las múltiples bandas de detalle sirven para mostrar diferentes datos de tablas hijas de una tabla principal que no estén relacionadas entre sí. Mientras, las agrupaciones de datos, permiten, agrupar datos por diferentes criterios a partir de tablas relacionadas entre sí con índices creados y activos en ese momento.

Bandas de Detalles Agrupaciones de Datos
No se anidan, son consecutivas Se pueden anidar
No se crean subgrupos dentro de ellas Se crean subgrupos
No dependen del alias (target alias). Se pueden mostrar independientemente de si existan o no registros que cumplan las condiciones en esa banda, por tanto pueden existir sin que haya banda de detalle asociado. Dependen del alias (target alias) que se le indique. No se muestran si no existe al menos un registro en la banda detalle de ese grupo
Su límite máximo es de 20 bandas Su límite máximo es de 74 bandas
Su límite mínimo es una banda Su límite mínimo es cero bandas
Cuando termina una banda, si existe otra banda de detalle el puntero se mueve nuevamente al inicio del alias actual Cuando acaba un grupo, el puntero se mueve al siguiente registro
La expresión para el alcance de una banda de detalle (target alias) se limita a una expresión que de nombre a una tabla abierta en la sesión de datos actual No existe límite para la creación de una expresión que defina el alcance de un grupo

Diferencias entre los encabezados y pies de Bandas de Detalle y los encabezados y pies de Bandas de Grupos

Encabezados y pies de Bandas de Detalle Encabezados y pies de Bandas de Grupos
Se pueden mostrar los encabezados y pie de banda (son opcionales). Si existen, se muestran incluso cuando no hay datos, por ejemplo que no haya coincidencia entre la tabla padre y la hija. Si no se desea imprimir el encabezado de la banda de detalle en este caso, debe utilizar una expresión del tipo NOT EOF(<target alias>) en Imprimir cuando (Print When) Si no hay elemento de agrupación en la selección, no se puede mostrar encabezado y/o pie para él, debido a que no existe tampoco el grupo como tal. Tiene que existir algún elemento, incluso si no se desea imprimir el detalle y se emplea la cláusula SUMMARY en el comando REPORT FORM, que se encarga de eliminarla.
La cláusula SUMMARY en el comando REPORT FORM, elimina todas las bandas de detalle, así como sus correspondientes encabezados y pies. La cláusula SUMMARY en el comando REPORT FORM, NO elimina los encabezados y pies de las bandas de grupo.
Si se incluye la cláusula SUMMARY, NO se ejecutan los eventos OnEntry / OnExit. Aunque se incluya la cláusula SUMMARY, los eventos OnEntry / OnExit SIEMPRE se ejecutan.

Los encabezados y pies de ambos tipos de bandas tienen características similares, por ejemplo la evaluación de su contenido y el código colocado en sus eventos OnEntry y OnExit ocurren en el mismo momento.

Variables y Cálculos del informe

Con la introducción de las múltiples bandas de detalle, las variables y cálculos del informe tienen algunas peculiaridades. Es necesario entender completamente como se procesan para utilizarlas con eficiencia. De otro modo, puede obtener resultados inesperados.

El cuadro desplegable Reiniciar basado en (Reset value based on) que vemos en el cuadro de diálogo Variables del informe determina que la variable se reinicia basada en los cambios del valor de la opción seleccionada. Si se define en el informe más de una banda, cada banda de detalle se agrega al cuadro desplegable.

Al seleccionar una Banda de detalle en particular se procesa este cálculo sólo para los registros del target alias de esta banda de detalle. La variable de informe no se altera mientras se procesan los registros en otros alias. Esto permite asociar una variable de informe para una banda de detalle en particular. El valor de la variable no se limpia hasta que el encabezado de esa misma banda de detalle se procesa para el siguiente registro de la tabla padre.

Si selecciona otro valor en Reiniciar en base a diferente a una banda de detalle, el cálculo se procesa en varios lugares. Primero, se aplica el cálculo para cada registro padre. Segundo, se aplica para cada registro en el alias destino de la primera banda de detalle. Luego, se aplica el cálculo para cada registro en el alias destino de la segunda banda de detalle, etc.

Informes sencillos

Un informe sencillo con múltiples bandas de detalle consiste en una tabla padre que conduce el informe y dos o más tablas hijas que están relacionadas con la tabla padre.

Un ejemplo de este caso, lo podemos ver en el panel Solutions, al que accedemos desde Menú - Herramientas - Administrador del panel de Tareas. A los ejemplos Solutions también se puede acceder desde la ventana de comandos

DO (HOME(2)+"Solution\Solution") 

Entonces, buscamos en el panel Solutions Samples, abrimos el árbol - New in Visual FoxPro 9.0 y buscamos "The typical multiple detail band report"

El aspecto del informe visto desde el Diseñador es el que se muestra en la siguiente figura. En ella se aprecian dos bandas de detalle asociadas a dos alias diferentes, agrupadas a su vez a nivel de la tabla principal, padre o conductora del informe. El resultado en este caso es un informe donde para cada Employee.EmployeeID se van a mostrar los datos que están almacenados en los alias employeeterritories y orders.

Cálculos (sum / count)

Un informe con múltiples bandas de detalle no necesariamente necesita tener múltiples tablas hijas. La misma tabla hija puede ser utilizada en más de una banda de detalle.

Por ejemplo para obtener Totales de grupos, antes de VFP 90, era muy difícil imprimir subtotales en la banda encabezado de grupo. El dato tenía que ser procesado previamente para calcular los totales antes de ejecutar el informe. Con VFP90, no se requiere procesamiento previo. Para ello la idea es crear bandas múltiples de detalle.

En la banda de detalle 1, TargetAlias = tabla hija. Una variante puede ser, marcar bandas de encabezado y pie y no colocar nada en la banda de detalle. En la banda pie para la banda de detalle 1, podemos agregar etiquetas y campos que serán calculados. Para ellos, en la ficha Cálculos de la ventana Propiedades, estableceremos: Iniciar basado en (Reset based on) Banda de detalle 1.

En la banda de detalle 2 TargetAlias = tabla hija, se marcan bandas de encabezado y pie y se agrega cualquier otro dato necesario a la banda de detalle 2.

La definición de informe descrita anteriormente, dice al Generador de informes de VFP 9.0, que procese dos veces la tabla hija para cada registro en la tabla Padre. La primera vez, realiza los cálculos que se hayan pedido (count y/o sum) y lo imprime. El segundo pase de la tabla hija imprime el detalle. Este proceso se repite para cada registro de la tabla Padre.

Otra opción puede ser no mostrar nada en la Banda 1, es decir, emplearla solamente para el cálculo. La banda 2 se encargaría entonces de mostrar los datos calculados por la Banda1.

Es muy importante comprender que el resultado de los cálculos a los que se asigne “Iniciar basado en (Reset based on) una Banda de detalle determinada, van a mantener su valor hasta el final de ejecución de todas las bandas y sólo cambiará en el momento que se vuelva a ejecutar la banda para la que se ha establecido su alcance.

Cálculos (porcentajes)

Otro concepto en los informes es mostrar el porcentaje del total de cada línea de detalle, en la medida en que se imprimen las líneas. Esto puede ser realizado también con múltiples bandas de detalle. Para ello, la idea es crear múltiples bandas de detalle. En la primera banda, TargetAlias = tabla hija, se marcan bandas de encabezado y pie y no se coloca nada en la banda de detalle.

Luego, crear algunas variables de informe, por ejemplo: una variable para el total donde Valor a almacenar igual a Tablahija.CampoATotalizar establezca Tipo de cálculo igual a Suma y Reiniciar basado en Detail 1. Y luego, crear una variable llamada pare el Porciento, donde Valor a almacenar igual a ROUND(100 * TablaHija.CampoATotalizar / nTotal, 2), Tipo de cálculo = Ninguno

Los datos van a estar en la banda de detalle 2, junto a un control que devuelve el resultado de la expresión nPorciento. En el pie de banda 2 se agrega el objeto Total, con expresión igual a Tablahija.CampoATotalizar, a Tipo de cálculo = Suma y Reiniciar basado en = Detail 2. Igual para Total Porciento, con expresión = nPorciento.

La definición de informe descrita anteriormente, dice al Generador de informes de VFP 9.0, que procese dos veces la tabla Hija para cada registro en la tabla Padre. La primera vez totaliza los valores para que puedan ser utilizados en el segundo pase. El segundo pase de la tabla Hija imprime el dato para el usuario, utilizando la variable de informe que fue calculada en la primera banda de detalle. Este proceso se repite para cada registro en la tabla Padre.

Volvemos a los ejemplos Solutions y encontramos para este caso "A multiple detail band report used for calculations"

En la siguiente figura se observan claramente cómo las dos bandas de detalle tienen el mismo alias. Lo que se hace en este caso es, que no se muestra nada en la Banda 1, la que es empleada solamente para el cálculo. La banda 2 se encarga de mostrar los datos calculados por la Banda1.

Sin dudas, es un paso muy grande de avance, con respecto a lo que existía antes de la versión VFP 9.0.

Por una parte, no es necesario ningún proceso previo para la creación de cursores y además, todos los campos son visibles en la ventana del Diseñador, lo que influye directa y favorablemente en la productividad y los tiempos dedicados al mantenimiento de los informes.

No obstante, hacerse con el control de las múltiples bandas de detalle puede tomar un tiempo. Hay que entender cómo trabajan todas las tablas juntas, establecer correctamente las alias para cada banda y es esencial e imprescindible establecer correctamente la relación entre las tablas. Debe tener también el control sobre cómo se afectan las variables y los cálculos por las múltiples bandas de detalle.

Espero que haya resultado de utilidad.

Saludos,

Ana María Bisbé York
www.amby.net


24 de junio de 2017

Múltiples bandas de detalle en informes (1)

En los foros apareció este comentario "Alguien ha realizado un reporte con dos detalles, es decir tengo 2 tablas que deseo que aparezcan en mi reporte... Que cuando termine la primera, a continuación se muestre la otra tabla"

Allí hubo una primera respuesta correcta. Vamos a ampliar un poco este tema, sobre la base de lo que hay en la beta pública de VFP 9.0. Seguramente, con el tiempo se podrá completar más; pero vamos asimilando las nuevas características de VFP 9.0 Debido a que este artículo se ha realizado con la versión Beta pública de VFP9, algunos aspectos pudieran sufrir modificaciones cuando se libere la versión definitiva.

Bandas multidetalles - Esta una de las mejoras más importantes de esta nueva versión. Veamos un ejemplo donde la tabla padre Clientes se enlaza con dos tablas hijas: Ordenes y Pagos. El objetivo del informe es obtener todos los datos agrupados del cliente. Creando 2 bandas de detalle, y teniendo las dos tablas hijas relacionadas con Cliente.dbf el resultado que obtenemos es el deseado según la pregunta planteada en el foro… " un reporte con dos detalles, 2 tablas que deseo que aparezcan en mi reporte...Que cuando termine la primera, a continuación se muestre la otra tabla"

¿Cómo se obtiene?

Definir multibandas - De forma predeterminada los informes se crean con una sola banda de detalle. Se pueden adicionar bandas desde el diálogo Bandas Opcionales (Optional Bands), incrementando el valor dentro del control Spinner (hasta 20). Luego, hay que definir el alias para cada banda, este aspecto lo veremos con detenimiento. Al aumentar un número aparece una nueva banda de detalle y al disminuir uno se elimina la banda con mayor número (la última en haber sido creada)

El trabajo multibandas nos ofrece nuevas posibilidades; pero nos exige ser mucho más cuidadosos, porque ahora en cada banda no estará la tabla padre sino una de las hijas, y hay que definir el nombre del alias en el cuadro de texto Expresión del Alias principal (Target alias expresion,) del Cuadro de diálogo Propiedades de la banda de detalle (Detail band propierties). En caso de estar trabajando con una sola banda, pues no pasa nada si la dejamos en blanco, toma la tabla padre que será justo lo que deseamos.


El cuadro de diálogo Detalles (Detail) contiene las propiedades de la banda de Detalles.

Alias principal (Target alias) - Es el alias que se recorre en la banda donde se encuentra. Es una expresión. Debe ir entre comillas, para que evalúe la expresión y obtenga los valores existentes en el Alias indicado. Si no se ponen las comillas, se entiende que es una expresión, por ejemplo Clientes, pudiendo ser una variable, o un campo que contiene el nombre del alias que se obtiene en tiempo de ejecución. Este nombre va a aparecer en la barra gris que define la banda. Se recomienda definir los campos incluyendo el nombre del alias para evitar confusiones. El tipo de alias dependerá del contenido que se quiera mostrar.

Nueva cláusula SUMMARY - Esta cláusula nos permite obtener sólo los encabezados y pie de cada banda sin mostrar el contenido de los detalles, es una buena opción, porque ahora el usuario podrá decidir si quiere la información compactada o detallada. Vea más adelante los resultados que se obtienen al emplearla.

Las variables y expresiones calculadas en el informe adquieren una nueva dimensión, Reset based on, en la ficha Variables de la ventana Propiedades indica que se deben procesar los registros de la tabla hija y no se limpia hasta que no termine el informe, lo que permite realizar cálculos y combinaciones entre las bandas utilizando valores calculados de unas en otras. Al seleccionar un número de banda específica, VFP procesa el cálculo solamente durante el proceso de esa banda. Si se escoge más de un sitio, pues se procesa mientras duren las bandas seleccionadas.

Diferencias entre las Bandas de Detalles y las Agrupaciones de Datos (Data Grouping)

Bandas de DetallesAgrupaciones de Datos
Se pueden anidarNo se anidan, son consecutivas
No se crean subgrupos dentro de ellasSe crean subgrupos
Dependen del alias principal (target alias) que se le indiqueNo dependen del alias principal (target alias)
Su límite máximo es de 20 bandasSu límite máximo es de 74 bandas
Su límite mínimo es una bandaSu límite mínimo es cero bandas
Cuando termina una banda, si existe otra banda de detalle el puntero se mueve nuevamente al inicio del alias actualCuando acaba un grupo, el puntero se mueve al siguiente registro
La expresión para el alcance de una banda de detalle (target alias) se limita a una expresión que de nombre a una tabla abierta en la sesión de datos actualNo existe límite para la creación de una expresión que defina el alcance de un grupo

Diferencias entre los encabezados y pies de Bandas de Detalle y los encabezados y pies de Bandas de Grupos

Encabezados y pies de Bandas de DetalleEncabezados y pies de Bandas de Grupos
Se pueden mostrar los encabezados y pie de banda (son opcionales). Si existen, se muestran incluso cuando no hay datos, por ejemplo que no haya coincidencia entre la tabla padre y la hija. Si no se desea imprimir el encabezado de la banda de detalle en este caso, debe utilizar una expresión del tipo NOT EOF(<target alias>) en Imprimir cuando (Print When)Si no hay elemento de agrupación en la selección, no se puede mostrar encabezado y/o pie para él, debido a que no existe tampoco el grupo como tal. Tiene que existir algún elemento, incluso si no se desea imprimir el detalle y se emplea la cláusula SUMMARY en el comando REPORT FORM, que se encarga de eliminarla.
La cláusula SUMMARY en el comando REPORT FORM, elimina todas las bandas de detalle, así como sus correspondientes encabezados y pies.La cláusula SUMMARY en el comando REPORT FORM, NO elimina los encabezados y pies de las bandas de grupo.
Si se incluye la cláusula SUMMARY, NO se ejecutan los eventos OnEntry / OnExit.Aunque se incluya la cláusula SUMMARY, los eventos OnEntry / OnExit SIEMPRE se ejecutan.

Los encabezados y pies de ambos tipos de bandas tienen características similares, por ejemplo la evaluación de su contenido y el código colocado en sus eventos OnEntry y OnExit ocurren en el mismo momento.

Otros Ejemplos

1. Con un único cursor.

Para poner los totales al inicio, en el encabezado de banda.

Hacer 2 bandas con igual alias, en la primera banda se calcula el total y se muestra el valor calculado en el encabezado. Pero esta banda no tiene más nada, no hay nada en el detalle ni el pie de banda.

Entonces, en la segunda banda están los datos en el detalle; lo importante es que VFP no está apuntando al último registro de esa tabla, por haber hecho SUM, COUNT, o la operación que sea, no, el puntero de la tabla al llegar a la segunda banda apunta al registro que corresponde por la relación padre-hija que existe con la tabla padre que es quien rige el informe.

El ejemplo concreto es imprimir para cada vendedor sus ventas (valor) y el porcentaje con relación al total, para lo cual necesitamos el total previamente calculado.

En este caso se utiliza una expresión representando el alias igual al alias principal (target alias) en la primera banda de detalle. Esto le dice al VFP que desea procesar todos los registros de la tabla antes de moverse a la siguiente banda de detalles.

2. Con múltiples cursores.

Cuando tiene múltiples cursores, utilice un alias principal (target alias) en blanco cuando desee que una banda sea procesada sólo una vez. Esto es útil si se desea mostrar información resumida, algo similar a lo que brinda el encabezado y pie de grupo.

Ejemplo:

Necesitamos agrupar el informe por ClienteID. Podemos ofrecer información sobre el Cliente en el Encabezado o pie de banda de grupo. Pero, podemos además colocar una banda de detalle entre Ordenes y Pagos que contenga información adicional sobre el Cliente, que puede ser, por ejemplo el estado actual de su cuenta. Al no colocar Alias principal (Target alias) su contenido aparecerá sólo antes de que se vaya a procesar la banda de Pagos. No hace falta escribir código en Imprimir cuando (Print When) ni seleccionar Eliminar datos repetidos (Suppress Repeated Value).

¿Qué ocurre en tiempo de ejecución? VFP verifica que el dato Alias principal (target alias) esté vacío o en uso, luego verifica que sea el alias actual o hija. Si el desarrollador tiene establecido su comportamiento de uno-a-muchos, por ejemplo con SET SKIP en su entorno de datos, el VFP lo respeta. Si no tiene definido nada, crea uno y lo elimina al finalizar el informe para dejar el entorno tal y como estaba.

  • Si la primera banda de detalles no tiene Alias principal (target alias), procesa el primer registro de la tabla principal y se mueve a la siguiente banda de detalles.
  • Si la primera banda de detalles tiene Alias principal (target alias) que es un alias hijo del alias principal, procesa todos los registros de la tabla hija mostrando los relacionados y pasa a la siguiente banda. El puntero permanece en el primer registro de la tabla principal.
  • Si la primera banda tiene el nombre de alias igual al nombre del alias principal, procesa todos los registros del alias principal y luego se mueve a la siguiente banda de detalles.

Limitaciones:

  • Mezclar múltiples columnas y múltiples bandas. Utilizar múltiples columnas en informes multibandas puede trabajar en algunos casos; pero pueden darse muchos casos de efectos desagradables si trata de darle columnas diferentes a las bandas y si no inician en página nueva para cada grupo y van de arriba abajo.
  • No es posible re-ordenar las bandas. No es un proceso nativo, pero se puede hacer eliminando y adicionando bandas y re-colocando los objetos en la posición que se desea. Se puede agrandar el tamaño de otras bandas, mover hacia ellas el contenido de la banda a eliminar re-crearla y luego re-posicionar los objetos. Se pueden seleccionar todos los objetos de la banda si se hace Doble-Clic en la banda separadora.

Como vemos, las posibilidades para trabajar los informes se amplían notablemente en esta nueva versión y aunque quedaría mucho por lograr, al menos, esta pregunta de los foros, ya tiene respuesta nativa en VFP 9.0

Saludos,

Ana María Bisbé York
http://www.amby.net

20 de junio de 2017

Emplear Hiperenlaces en informes

Artículo original: Hyperlinks Your Reports
Autor: Doug Hennig
Traducido por: Ana María Bisbé York


El mes pasado, Doug Hennig abordó la nueva clase ReportListener en VFP 9 y cómo puede ser utilizada para controlar la salida de informes de formas que anteriormente eran imposibles. Este mes, habla sobre cómo agregar hiperenlaces vivos a la salida generada de informes, permitiendo algunas acciones que se pueden realizar cuando se hace clic sobre ellos.

¿No sería bueno poderle decir a VFP que agregue hiperenlaces a un campo en un informe? Entonces, el cliente podría hacer Clic sobre el hiperenlace para navegar a alguna información relacionada. Por ejemplo, un informe que muestre los clientes y sus sitios Web o sus direcciones de correo electrónico podría tener enlaces vivos, haciendo clic a sus sitios Web podría navegar el examinador (browser) a esta URL.

Incluso, mucho más interesante sería tener la posibilidad de navegar a algún otro lugar dentro de su propia aplicación. Por ejemplo, haciendo clic en un nombre de compañía en un informe podría saltar a un formulario de entrada de datos del cliente para el que ha seleccionado la compañía.

Debido a que la vista preliminar del informe que viene con VFP no admite objetos vivos, sobre los que se pueda hacer clic en un informe, la vía más sencilla de implementarlo es usando HTML, que admite hiperenlaces de forma nativa.

Hiperenlazando informes

VFP viene con un report listener que crea HTML (la clase HTMLListener generada en la aplicación ReportOutput.APP e incluida también en _ReportListner.VCX en la carpeta  FFC), pero yo estaba seguro que necesitaríamos mucho trabajo para lograr el resultado con hiperenlaces. Sin embargo, me sorprendí gratamente al descubrir cuán poco esfuerzo hace falta para ello.

Primeramente, una pequeña introducción. HTMLListener es una subclase de XMLDisplayListener, el que a su vez es una subclase de XMLListener y esta es una subclase de _ReportListener, la clase que he descrito el mes pasado y recomiendo su uso normalmente como clase padre para sus propios listeners.

(Nota de la traductora: Este artículo fue publicado en la Revista FoxTalk 2.0 en  Marzo-2005. El autor se refiere a un artículo publicado en el mes de Febrero en la propia revista bajo el título Lintening to a Report, recientemente ha sido también liberado y se encuentra disponible en las páginas de Microsoft enhttp://msdn.microsoft.com/library/en-us/dnfoxtk05/html/ft05b6.asp.

Cuando utiliza HTMLListener, ya sea directamente intanciándolo y utilizándolo como un listener para un informe, o al especificar OBJECT TYPE 5 en el comando REPORT, en realidad genera un XML para el informe (esto lo hace la clase padre), entonces aplica una transformación XSL del XML para crear un HTML. El XSLT que utiliza está definido en el método GetDefaultUserXSLTAsString.

El XSLT predeterminado utilizado por HTMLListener es muy complejo, y no es mucho más que un experto XSL, yo pensé que podría ser una tarea agobiante determinar qué cambios se debían agregar para que admitiera hiperenlaces. Sin embargo, comencé a escarbar en GetDefaultUserXSLTAsString, y he descubierto lo siguiente:

<xsl:when test="string-length(@href) > 0">
<A href="{@href}">
<xsl:call-template name="replaceText"/>
</A>
</xsl:when>

Este XSL agrega una etiqueta al HTML si existe un atributo HREF en el elemento actual en el XML.Esto es genial - significa que el HTMLListener ya admite hyperlinks! Sin embargo, no hay búsquedas por "HREF" en XMLDisplayListener ni XMLListener, entonces, ¿cómo se puede agregar este atributo a un elemento, especialmente de forma dinámica?

Antes de avanzar mucho más, encontré que los atributos para un elemento en particular se configuran en el métodoGetRawFormattingInfo de XMLListener. Entonces, he subclaseado HTMLListener y añadido el comportamiento deseado a este método.

El siguiente código, tomado de HyperlinkListener.PRG, proporciona un listener que genera un hiperenlace sobre un objeto en el informe si el campo User memo de este objeto contiene la directiva "*:URL =" seguida por la expresión que utiliza como URL

define class HyperlinkListener as HTMLListener ;
  of home() + 'ffc\_ReportListener.vcx'
  QuietMode = .T.
  && QuietMode queda predeterminado para que no haya retorno 
  dimension aRecords[1]
  && matriz de información de cada registro FRX
  * Antes de ejecutar el informe, se recorre el FRX y
  * se guarda información sobre cada registro con sus directivas
  * en el campo memo USER en la matriz aRecords.
  function BeforeReport
    dodefault()
    with This
      .SetFRXDataSession()
      dimension .aRecords[reccount()]
      scan for atc('*:URL', USER) > 0
        .aRecords[recno()] = ;
          alltrim(strextract(USER, '*:URL =', ;
          chr(13), 1, 3))
      endscan for atc('*:URL', USER) > 0
      .ResetDataSession()
    endwith
  endfunc
  * Si el campo actual tiene una directiva, añade el URL
  * a los atributos del nodo.
  function GetRawFormattingInfo(tnLeft, tnTop, ;
    tnWidth, tnHeight, tnObjectContinuationType)
    local lcInfo, ;
    lnURL
    with This
      lcInfo = dodefault(tnLeft, tnTop, tnWidth, ;
        tnHeight, tnObjectContinuationType)
      lcURL = .aRecords[recno('FRX')]
      if not empty(lcURL)
        .SetCurrentDataSession()
        lcInfo = lcInfo + ' href="' + ;
          textmerge(lcURL) + '"'
       .ResetDataSession()
     endif not empty(lcURL)
    endwith
    return lcInfo
  endfunc
enddefine

El evento BeforeReport se dispara justo antes de que se ejecute el informe. Utiliza el método SetFRXDataSession para seleccionar la sesión de datos en la que está el cursor FRX y luego busca a través del FRX y coloca la expresión URL para cada objeto que tiene la directiva en una matriz. Llama a ResetDataSession al final para restaurar la sesión de datos en la que está el listener.

El método GetRawFormattingInfo utiliza DODEFAULT() para configurar el comportamiento habitual, el que genera los atributos para un elemento XML como una cadena. Esto entonces verifica el elemento de matriz apropiado (la sesión de datos del cursor FRX se seleccionó por código en XMLListener antes de que se ejecute este código) para ver si el objeto actual del informe tiene la directiva y si es así, agrega un atributo HREF al elemento XML. Llama a SetCurrentDataSession para seleccionar la sesión de datos utilizada por los datos del informe y utiliza TEXTMERGE() sobre la expresión URL porque la expresión va a contener algo específico para cada registro, algo como <<CustomerID>>. Finalmente, realiza labores esenciales de mantenimiento al llamar a .ResetDataSession() para dejar la sesión de datos tal y como la encontramos.

¡¡ Es todo !! Veamos ahora algunos ejemplos de cómo podemos utilizar este listener.

Ejemplo 1: Enlaces vivos a URLs

Enlaces.FRX es un ejemplo sencillo que muestra cómo trabaja este listener. Es un informe sobre la tabla Links, que contiene una lista de nombres de compañías y sus sitios Web. El campo website en el informe tiene  "*:URL = http://<<trim(website)>>" en el campo User memo. El programa Links.PRG ejecuta este informe, utilizando HyperlinkListener como el report listener, y utiliza la clase _ShellExecute en las FFC para mostrar el archivo HTML en su examinador predeterminado. La Figura 1 muestra los resultados.

loListener = newobject('HyperlinkListener', ;
  'HyperlinkListener.prg')
loListener.TargetFileName = fullpath('Links.html')
report form Links object loListener
loShell = newobject('_ShellExecute', ;
  home() + 'ffc\_Environ.vcx')
loShell.ShellExecute(loListener.TargetFileName)
Figura 1

Ejemplo 2: Informes tipo Drilldown (cambio rápido)

HyperlinkReports.SCX  es un ejemplo más complejo. Como puede ver en la Figura 2, presenta una lista de información de cliente. Sin embargo, este HTML es mostrado en un control ActiveX Web Browser embebido en un formulario VFP en lugar de una ventana examinar. Al hacer clic en un nombre de compañía, VFP ejecuta un informe de las órdenes de ese cliente y los muestra en el formulario como se observa en la Figura 3. El informe de las órdenes tiene también hiperenlaces que vuelven a mostrar la lista de clientes. Entonces, este formulario proporciona informes del tipo drilldown (cambio rápido).

Figura 2 y Figura 3

El método Init de un formulario utiliza la clase HyperlinkListener para generar un archivo HTML hiperenlazado desde el informe HyperlinkCustomers y llama al método ShowReport para mostrarlo en el control Web Browser. Además, mantiene una colección de archivos HTML generados por el formulario, de tal forma que puedan ser eliminados cuando se cierra el formulario.

with This
  * Crea una colección de los archivos HTML que hemos creado
  * para que podamos nuclearlos (nuke) todos cuando hayamos cerrado.
  .oFiles = createobject('Collection')
  * Create the customers report.
  .oListener = newobject('HyperlinkListener', ;
    'HyperlinkListener.prg')
  .oListener.TargetFileName = ;
    fullpath('HyperlinkCustomers.html')
  report form HyperlinkCustomers object .oListener
  .oFiles.Add(.oListener.TargetFileName)
  * Display it.
  .ShowReport()
endwith

El método ShowReport simplemente comunica al control Web Browser que cargue el archivo HTML actual:

local lcFile
lcFile = This.oListener.TargetFileName
This.oBrowser.Navigate2(lcFile)

En lugar de generar informes de órdenes para cada cliente e hiperenlazarlos, he decidido generar los informes en dependencia del pedido que se haga en cada momento, al hacer clic sobre el nombre de cliente. Para hacer esto, necesité interceptar el clic al hiperlink. Afortunadamente, esto es fácil de hacer: Sencillamente coloqué código en el evento BeforeNavigate2 del control Web Browser.

Para decirle a BeforeNavigate2 que no es un hiperenlace normal, he utilizado la convención de "vfps://," el que  entiende por "VFP script," en lugar de "http://." El código para BeforeNavigate2  busca esta cadena en la URL y si la encuentra, ejecuta el código en el resto de la URL en lugar de estar navegando por ella. Por ejemplo, el campo memo User del campo CompanyName  en el HyperlinkCustomers.FRX  tiene lo siguiente:

*:URL = vfps://Thisform.ShowOrdersForCustomer('<<CustomerID>>')

El report listener HyperlinkListener convertirá esto en una etiqueta como <a href="vfps://Thisform.ShowOrdersforCustomer('ALFKI')">  para el cliente cuyo CustomerID es ALFKI. Al hacer clic en este hiperenlace en el control Web Browser, BeforeNavigate2 se dispara y el código de este evento se desentiende de este pedazo "vfps://" y ejecuta el resto. Además, establece el parámetro Cancel pasado por referencia, a .T. para indicar que la navegación normal no puede tener lugar (similar a utilizar NODEFAULT en un método VFP). He aquí el código para BeforeNavigate2:

LPARAMETERS pdisp, url, flags, targetframename, ;
  postdata, headers, cancel
local lcMethod
if url = 'vfps://'
  lcMethod = substr(url, 8)
  lcMethod = left(lcMethod, len(lcMethod) - 1)
  && strip trailing /
  &lcMethod
  cancel = .T.
endif url = 'vfps://'

El método ShowOrdersForCustomer , ejecutado al hacer Clic en un nombre de compañía, ejecuta el informe HyperlinkOrders para un cliente especificado, lo muestra en el control Web Browser, y agrega el nombre de archivo a la colección de archivos a ser eliminados cuando se cierra el formulario.

lparameters tcCustomerID
with This
  .oListener.TargetFileName = ;
    fullpath(tcCustomerID + '.html')
  report form HyperlinkOrders object .oListener ;
    for Orders.CustomerID = tcCustomerID
  .ShowReport()
  .oFiles.Add(.oListener.TargetFileName)
endwith

El campo CustomerName  en el informe HyperlinkOrders tiene  "*:URL = vfps://Thisform.ShowCustomers()"  en el campo memo User, así que al hacer clic en este hiperenlace en el informe vuelve a mostrar la lista de clientes.

Ejemplo 3 Lanzar un formulario VFP.

El formulario CustomerReport.SCX  es similar al HyperlinkReports.SCX; pero es un poco más sencillo. Contiene además un control Web Browser que muestra el HTML desde un informe, EditCustomers.FRX, el que tiene el mismo aspecto que el ejemplo anterior. Sin embargo, al hacer clic en un nombre de cliente en este formulario, muestra un mantenimiento para el cliente seleccionado.

El informe EditCustomers.FRX  es un clon del informe HyperlinkCustomers utilizado en el ejemplo anterior; pero tiene en su lugar  User "*:URL = vfps://Thisform.EditCustomer('<<CustomerID>>')" en el campo memo del campo Companyname. El método EditCustomer llamado desde BeforeNavigate2 cuando se hace clic en un nombre de cliente, lanza un formulario Customers, pasándole el CustomerID para el cliente seleccionado. El formulario Customers es un mantenimiento sencillo de la tabla Customers, con controles enlazados a cada campo y botones Save (Guardar) y Cancel (Cancelar).

NavPaneListener

El MVP Fabio Vázquez ha creado otro tipo de listener que tiene hiperenlaces,  aunque para un propósito completamente diferente. Su NavPaneListener, disponible para descarga desdehttp://ReportListener.com, ofrece una vista previa de informe HTML con una tabla de contenidos para el informe. Como puede ver en la Figura 4, a la izquierda se muestra una vista en miniatura (thumbnail) de cada página y la página actual se muestra a la derecha. Al hacer clic sobre la vista en miniatura navega a la página adecuada.

Figura 4

Al igual que HyperlinkListener, NavPaneListener es bastante sencillo. Su evento OutputPage, llamado cuando va a salir cada página, genera simplemente un archivo GIF para la página al llamarse a si mismo nuevamente con los parámetros adecuados. tnDeviceType  inicialmente es -1, lo que significa que no hay salida, ya que el ListenerType del listener es igual a 2. En ese caso, OutputPage se llama a si mismo, pasando el nombre de la ruta para un GIF para generar (la propiedad cPath predetermina la ruta actual) y un tipo de dispositivo que indica un archivo GIF. En la segunda llamada, además del comportamiento normal (generar el GIF especificado, OutputPage pasa el nombre y la ruta al método AddPage de un objeto colaborador guardado en la propiedad oNavigator. (Nota: He traducido un poco el código de Fabio a inglés para hacerlo más legible.)

procedure OutputPage(tnPageNo, teDevice, ;
  tnDeviceType)
  local lnDeviceType
  with This
    do case 
      case tnDeviceType = -1 && None
        lnDeviceType = 103 && GIF
        .OutputPage(tnPageNo, .cPath + 'Page' + ;
          transform(tnPageNo) + '.gif', lnDeviceType)
      case tnDeviceType = 103
        .oNavigator.AddPage(teDevice)
    endcase
  endwith
endproc

Después de que está listo el informe, el objeto navegador crea un par de documentos HTML - un documento que define un conjunto de paneles  con la tabla de contenidos en el panel de la izquierda y los contenidos a mostrar en el derecho y otro documento que contiene la tabla de contenidos como vistas en miniatura del GIF hiperenlazado para mostrar el archivo GIF de tamaño completo. El objeto navegador automatiza luego el Explorador de Internet para mostrar el conjunto de paneles de documentos.

Conclusiones

Observe que nada en e código en ninguno de estos ejemplos es complicado, ni tampoco hay mucho que hacer. Como resultado, toma solo unos momentos implementar informes con hiperenlaces vivos, informes tipo drilldowns, informes que lanzan otros formularios VFP y otras acciones, o informes con paneles de navegación. ¡ Esto muestra verdaderamente el poder de los report listeners !

El próximo mes, veremos un tópico similar -  vista preliminar de informes que permitan realizar algunas acciones cuando se hace clic al utilizar una técnica completamente diferente que nos brindará posibilidades como son búsqueda de texto y marcadores.

16 de junio de 2017

Poner la ventana principal de VFP en el primer plano

Función API para poner la Ventana Principal de VFP en el primer plano fijo, situa nuestra aplicación por encima de cualquier otra que se encuentre abierta en Windows.

***************************************************************************
***************************************************************************
*
*   Función    : VentanaTopMost
*   Proposito  : Pasar la ventana Principal de VFP al primer plano fijo
*                Situar nuestra aplicación siempre encima de todas las 
*                demas ventanas de windows
*   Parametros : 1 - Pasar la ventana Principal de VFP al primer plano fijo
*                0 - Quitar la ventana Principal de VFP del primer plano fijo
*   Regresa    : Nada
*   Ejemplo    : =VentanaTopMost(1)
*     
***************************************************************************
***************************************************************************
FUNCTION VentanaTopMost(n_Estado)
DECLARE Integer SetWindowPos IN WIN32API ;
      Integer  nWnd, ;
      Integer  nWndInsertAfter, ;
      Integer  nTop, ;
      Integer  nLeft, ;
      Integer  nHeight, ;
      Integer  nWidth, ;
      Integer  nFlags
 
DECLARE INTEGER FindWindow IN WIN32API ;
STRING cNULL, ;
STRING cWinName
      
#define SWP_NOSIZE          1
#define SWP_NOMOVE          2
#define HWND_TOPMOST       -1
#define HWND_NOTOPMOST     -2
 
*--- se obtiene el manejador de la ventana principal
n_FoxHwnd = FindWindow( 0, _SCREEN.Caption )
 
*--- si el parametro es 1
IF n_Estado = 1
 
   *--- pasar a primer plano fijo
   =SetWindowPos(n_FoxHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE + SWP_NOMOVE )
 
ENDIF
 
*--- si el parametro es 0
IF n_Estado = 0
 
   *--- la quita del primer plano fijo
   =SetWindowPos(n_FoxHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE + SWP_NOMOVE )
 
ENDIF

ENDFUNC
***************************************************************************
***************************************************************************

Carlos Tarello (Puebla, Mexico)

13 de junio de 2017

El entorno de datos en las clases.

No se si les pasó a ustedes el tener que repetir una y otra vez el alta de las mismas tablas en el entorno de datos para similares ABM de datos, al crear un form de datos fijos repetitivos como clientes, proveedores, localidades, etc.

Si hacia una clase para cada una de estas tablas para luego crear el form de nuestro cliente derivado de la clase escrita, siempre me faltaba algo. Trabajar en los campos. (agregarlos en el form, o editar su ControlSource) y crear el entorno de datos en el formulario.

Todos los proveedores tienen campos similares, nombre, domicilio, etc. Y todas esas tablas se llaman igual. ¿Por qué no crear entonces un entorno de datos en la clase?

Lo que consideré al hacer esto fue:

  • Tablas similares deben usarse con un mismo nombre (aunque pertenezcan a bases de datos distintas) Ej. articulos.dbf, rubros.dbf, etc.
  • Estas tablas tienen siempre similares estructuras. (en algunos casos se agregan o quitan campos)
  • Ubicaciones en directorios diferentes y nombre de bases de datos diferentes.

1 - Cree una clase no visual visible para todos los formularios:

Define Class Cursores_01 As Cursor Name = "Cursores_01"
 Exclusive = .F.
 ReadOnly = .F.
 *- esta propiedad que sigue, es de lectura y escritura en modo de ejecucion:
 *-  _oSis.cDbc nombre de la base de datos de la aplicación del framework.
 Database = Juststem(_oSis.cDbc)+".DBC" 
 Procedure Init
  Lparameters tcTabla
  Set Exclusive Off
  Set Multilocks On
  tcTabla = Juststem(tcTabla)
  If !File(This.Database)
   Messagebox(This.Database+" no existe.",48,This.Name)
   Return .F.
  Endif
  If !File(tcTabla+'.dbf')
   Messagebox("No se puede encontrar: "+tcTabla+".dbf",16,This.Name)
   Return .F.
  Endif
  This.Alias = tcTabla
  This.CursorSource = tcTabla  && nombre largo de la tabla
  This.Comment = 'Cursor '+tcTabla
 Endproc
Enddefine

2 - En el método LOAD de la clase form escribí;

*- Ej. ABM de datos para proveedores:
If This.DataSession # 2
 Messagebox('No ha especificado sesion privada de datos',16,This.Name)
 Return .F.
Endif
If !"CLASES_01"$Set("Procedure")
 Set Procedure To Clases_01 Additive
Endif
Set Deleted On

With Thisform.DataEnvironment
 .CloseTables() && libera el entorno de datos cargado
 .AddObject("CurProveedores", "Cursores_01", ‘proveedores’)
 .AddObject("CurLocalidades", "Cursores_01", ‘Localidades’)
 *- ... etc. y todas las tablas que se necesitan en el form.
 .OpenTables()
Endwith
Return This.nError = 0
*- Fin Load.

(a partir de aqui esta clase ya puede tener todos los campos y sus ControlSource establecidos en tiempo de diseño y funcionará sin ninguna modificación adicional en el formulario que, de entrada no tendrá ni una linea de código y funcionará.)

3 - Crear el form derivado de esta clase de abm y establecer DataSession = 2

Guardar el formulario en el directorio del cliente (o aplicación) para modificar en el futuro las especificaciones exclusivas del cliente en el. (como agregar campos, cambiar Valid, etc.)

Los problemas del futuros podrían ser:

Agregar campos a la tabla: Siempre se puede sobrescribir el método load del form, copiando y pegando el de la clase y agregando el o los cursores adicionales.

Quitar campos: No implica problema en los pocos casos en que esto sucede se puede usar la propiedad Visible del campo. La tabla no hay porque modificarla.

Espero que les sirva como me sirvió a mi.

Saludos.

Alberto Rodriguez

8 de junio de 2017

Opciones alternativa a la función FILE()

Como se ha visto últimamente en los foros de noticias de microsoft, la función FILE() tiene algunos "errores" que pueden afectar el comportamiento de nuestros programas, aquí encontrarás algunos métodos adicionales.

Normalmente, la función File() la utilizamos para revisar si existe un archivo en cierta ruta, pero el comportamiento de la misma función tiene sus detalles que están documentados en la ayuda:

"Si el archivo no puede encontrarse en el directorio predeterminado, se buscará en la ruta de acceso de Visual FoxPro establecida con SET PATH. "

Que significa esto? Que aunque se le indique la ruta completa de un archivo a buscar, la función File() buscará también ese mismo archivo dentro de la ruta establecida por PATH, dándole un tiempo adicional en hacer su labor. Por ejemplo:

Caso 1.

Intentamos buscar el archivo "HolaMundo.txt" en una ruta especifica:

? FILE("c:\miApp\miDirectorio\HolaMundo.txt")

Si el archivo no existe en ese directorio devolverá .F., pero..., si algún archivo llamado "HolaMundo.txt" se encuentra en algunas de las rutas establecidas por el comando SET PATH, o en el directorio de trabajo de la aplicación, entonces devolverá .T., cuestión que puede afectar en gran medida el comportamiento de nuestro sistema.

Caso 2

Buscamos el archivo "HolaMundo.txt" sin poner ruta completa para hacer validaciones de X tipo:

IF File("HolaMundo.txt")
   Messagebox("... blah blah blah....")
ENDIF

Si por alguna razón, nuestro archivo se encuentra en cualquier otra localidad establecida por SET PATH, la función también devolverá .T., una vez más, caso dificil de controlar, si suponemos que podemos tener cientos de rutas establecidas.

Como es de imaginarse, los inconvenientes que puede ocasionar este comportamient normal de la función FILE() puede ser algo engorroso.

Muy bien, que hay por hacer? Hasta el momento, gracias a la colaboración que ha surgido en los mensajes de los newsgroup de microsoft, tenemos lo siguiente:

Opción 1

Quitar las rutas establecidas por SetPath antes de utilizar la función File():

Function SureFile
  LParameters tcFileName
  Local lcOldPath
  Local llRetValue 
       lcOldPath = SET("PATH")
       SET PATH TO
          llRetValue = File(m.tcFileName)
       SET PATH TO &lcOldPath
      Return llRetValue
EndFunction

Opcion 2

Utilizar la función ADIR(), la cual, no presenta el inconveniente de la tan mencionada función FILE().

Function SureFile
  LParameters tcFileName
  RETURN (ADIR(laDummy, m.tcFileName) > 0)
EndFunction

Opcion 3

Utilizando la función SYS(2000):

Function SureFile
  LParameters tcFileName
  RETURN NOT EMPTY(SYS(2000,m.tcFileName))
ENdFunction

Opcion 4

Utilizar una API para llevar a cabo la labor:

*** En tu programa de inicio, solo una vez *****
declare Integer GetFileAttributes in win32api string @
****************************************

Function SureFile
   LParameters tcFileName
       return (GetFileAttributes(@m.tcFileName)  <> -1)
EndFunction

Esta última opción es la que he visto funciona un poco más rapido que las anteriores, ya que no hay que re-establecer ningún setting, ni crear ningún arreglo, ni buscar nada que no sea exactamente lo que deseamos.

Así pues, pongo a consideración las opciones disponibles, hagamos nuestras y pruebas y si tienen algún comentario al respecto, comentemoslo ya sea aquí, o en los newsgroups de microsoft (son públicos y gratuitos).

Espero que la información les sea de utilidad.

Espartaco Palma Martínez

3 de junio de 2017

MemberData y los editores de propiedades de usuario

Artículo original: MemberData and Custom Property Editors
Autor: Doug Hennig
Traducido por: Ana María Bisbé York


Un par de peticiones frecuentes de los desarrolladores VFP eran la posibilidad de contar con letras mayúsculas en las propiedades y métodos de usuario en la ventana Propiedades y en IntelliSense, y la posibilidad de personalizar la ventana Propiedades. Microsoft ha escuchado estas peticiones: VFP 9 ofrece ambas posibilidades, más la capacidad de crear un editor de propiedades propio, con una nueva característica llamada MemberData. Lo explica Doug Henning.

Algo que me ha molestado de VFP desde sus inicios es que las propiedades y métodos de usuario eran forzados a minúsculas en archivos VCX o SCX. Esto hace que aparezcan al final de la ventana Propiedades en lugar de intercaladas con las propiedades, eventos y métodos nativos (referidos como PEMs o miembros). Significa además que IntelliSense no muestra los nombres de los miembros con mayúsculas en las letras significativas, entonces, siempre terminaba corrigiendo el nombre que IntelliSense insertaba por mi. Deseaba además que existiera una vía para limitar la ventana Propiedades de forma tal que mostrara justamente aquellos PEMs que me interesaran, no los cientos que yo nunca utilizo.

VFP 9 ha respondido a mis plegarias. Esta nueva versión proporciona una vía para especificar metadato de las clases miembros. Este metadato, referido como MemberData, contiene atributos tales como el tipo de letra (mayúscula / minúscula) utilizado para mostrar el miembro (el nombre aún se guarda físicamente en minúsculas en el archivo VCX o SCX) y si se muestra o no un miembro en la nueva ficha Favoritos (Favorites) de la ventana Propiedades (Properties).

MemberData se implementa agregando una nueva propiedad llamada _MemberData a una clase y rellenándola con XML de forma tal que contenga el metadato para los miembros de la clase. La propiedad _MemberData no se agrega a la clase automáticamente, ni se llena el XML, es necesario hacer ambas cosas por si mismo (luego, en este artículo, les diré cómo hacerlo).

MemberData

He aquí un esquema XSD para MemberData:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="VFPData">
<xs:complexType>
<xs:sequence>
<xs:element name="memberdata">
<xs:complexType>
<xs:attribute name="name" type="xs:string"
use="required" />
<xs:attribute name="type" type="xs:string"/>
<xs:attribute name="display" type="xs:string"/>
<xs:attribute name="favorites" type="xs:boolean"/>
<xs:attribute name="override" type="xs:boolean"/>
<xs:attribute name="script" type="xs:string"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

Bien, ahora lo digo en español. La Tabla 1 muestra los atributos que estructuran el metadata de un miembro.

AtributoDescripción
nameNombre del miembro.
typeTipo de miembro: "property", "event" o "method".
displayTexto que se utiliza como nombre del miembro en la ventana Propiedades y en IntelliSense.
favorites"True" si el miembro debe aparecer en la ficha Favoritos de la ventana Propiedades o "False" en caso contrario.
override"True" para ignorar el metadata de la clase padre o "False" para heredarlo de la clase padre.
scriptCódigo para ejecutar cuando se selecciona el botón de edición de la propiedad para este miembro en la ventana Propiedades.

Tabla 1. Los atributos del MemberData contienen el metadata para un miembro.

He aquí algunas anotaciones sobre estos elementos:

  • Debido a que es un XML, los nombres de los elementos y atributos son sensibles a las diferencias entre mayúsculas y minúsculas (case-sensitive). Con la excepción del atributo script, el contenido de los atributos también son case-sensitive. El nombre y tipo de los atributos debe estar en minúsculas. "True" y "False" deben ser especificados con letra inicial mayúscula en los atributos favorites y override.
  • El atributo display no puede ser diferente del nombre de los miembros, excepto en tipo de letra (mayúscula / minúscula). Por ejemplo, mientras se acepta "MiPropiedad" para el atributo display del miembro mipropiedad, "CualquierOtraCosa" no lo es.
  • Puede modificar MemberData en subclases sin tener que reproducir la cadena XML entera, los atributos no especificados toman sus valores de la clase padre. Sin embargo, si el atributo override es "True," para los atributos no especificados se utiliza el comportamiento predeterminado. Por ejemplo, suponga que tiene una clase con un metadato especificando un atributo display igual a MiPropiedad. Si el atributo display de la subclase está omitido, heredará de la clase padre. Sin embargo si el atributo override es "True," la propiedad se mostrará como "mipropiedad" en la ventana Propiedades porque el atributo display no hereda de la clase padre y no ha sido especificado en esta clase.
  • Si el atributo script está especificado para un miembro, se verá el botón de edición de la propiedad en la ventana Propiedades al seleccionar el miembro. Al hacer Clic en ese botón se ejecuta el código especificado en el atributo script, utilizando EXECSCRIPT().
  • Si el XML no es válido, no se recibe un mensaje de error; pero tampoco se verán los efectos de MemberData.

He aquí un ejemplo que indica que la propiedad mycustomproperty debe mostrarse como MyCustomProperty y debe aparecer en la ficha Favoritos de la ventana Propiedades, y VFP debe llamar a MyCustomPropertyEditor.PRG cuando se haga clic en el botón de edición de la propiedad en la ventana Propiedades.

<?xml version = "1.0" encoding="Windows-1252"
standalone="yes"?>
<VFPData>
<memberdata
name="mycustomproperty"
type="property"
display="MyCustomProperty"
favorites="True"
override="False"
script="DO MyCustomPropertyEditor.PRG"/>
</VFPData>


Figura 1


Figura 2


Figura 3

La Figura 1 muestra cómo aparece esta propiedad en la ventana Propiedades. Observe que aparece como MyCustomProperty, se encuentra en el lugar adecuado en orden alfabético en lugar de al final de la ventana Propiedades, y un botón de edición de propiedad aparece a la derecha del cuadro de texto del valor. En la Figura 2 puede ver que esta propiedad es una de las pocas mostrada en la ficha Favoritos. La Figura 3 muestra cómo aparece la propiedad en IntelliSense.

MemberData Global

Es fantástico tener una clase MemberData específica; pero sería un obstáculo si tiene que especificar el mismo metadato para las mismas propiedades en todas las clases. Por ejemplo, si agrega la propiedad MyCustomProperty a varias clases, deseará utilizar el mismo metadato para cada clase. Afortunadamente el equipo MS VFP ha pensado en esto.

Para crear un MemberData a nivel global (quiere decir, para todas las clases que tienen este miembro), agregue un registro a la tabla IntelliSense que especifique el MemberData. (La tabla IntelliSense, especificada en la variable de sistema _FOXCODE, de forma predeterminada se llama FOXCODE.DBF y se encuentra en el directorio, que es devuelto por la función HOME(7)).

Establezca el campo TYPE igual a "E" (un valor nuevo en VFP 9 que denota un registro MemberData), coloque el nombre de los miembros en ABBREV (no es importante tener en cuenta mayúsculas y minúsculas), y ponga el MemberData en TIP. Si la propiedad _MemberData para un objeto contiene metadato para un miembro que tiene también un metadato global, la _MemberData sobrescribirá la global.

He aquí un ejemplo que crea un MemberData global para MyCustomProperty al agregar al FOXCODE un registro con la información correcta:
 

local lcXML
* Crear el MemberData.
text to lcXML noshow
<?xml version="1.0" encoding="Windows-1252"
standalone="yes"?>
<VFPData>
<memberdata
name="mycustomproperty"
type="property"
display="MyCustomProperty"
favorites="True"
override="False"
script="do MyCustomPropertyEditor.PRG"/>
</VFPData>
endtext
* Crear ahora un registro en FOXCODE que proporcione el
* MemberData para MyCustomProperty.
use (_foxcode) again shared alias FOXCODE
insert into FOXCODE (TYPE, ABBREV, TIP) values ('E', ;
'MyCustomProperty', lcXML)
use

Haga una prueba: Ejecute el código anterior (GlobalMemberData.PRG en el archivo Descargar) y luego escriba lo siguiente en la ventana Comandos:

create class x of x as custom

Cree una propiedad llamada MyCustomProperty y obseve que aun cuando no existe la propiedad _MemberData para esta clase, MyCustomProperty se muestra con las letras correctas y en la ficha Favoritos.

MemberData no se aplica solamente a las propiedades y métodos de usuario. Por ejemplo, debido a que las propiedades Caption y Name son las que cambio con más frecuencia en formularios, etiquetas, casillas de verificación y otros objetos con estas propiedades, voy a crear un metadato global para agregar estas propiedades a la ficha Favoritos de tal forma que no tenga que saltar por la ventana Propiedades para encontrarlas. Esto me brinda un pequeño empujón de productividad para cada objeto durante toda la vida del proyecto, lo que sumado, puede ser un ahorro de tiempo considerable.

Editores de propiedades (Property editors)

Igual que un generador, un editor de propiedad puede facilitar la entrada del valor de la propiedad. Por ejemplo, la nueva propiedad Anchor define cómo reacciona un control cuando el contenedor es redimensionado (por ejemplo, puede ser redimensionado o movido). Sin embargo, el valor de Anchor es una suma de valores numéricos, por ejemplo 12 (8, anclado a la derecha + 4, anclado al borde inferior). Esto no es muy intuitivo, así que un editor de propiedad puede ayudar mucho en la productividad.

Aunque puede especificar múltiples líneas de código en el atributo script del metadato, normalmente tiene más sentido llamar a un PRF o un formulario. Para un MemberData global, puede especificar además que desea ejecutar un registro script en FOXCODE utilizando lo siguiente como atributo de script:

do (_CODESENSE) with 'RunPropertyEditor', '', 'SomeValue'

SomeValue es el valor que desea pasar al script. Ponga "{ScriptName}" en CMD, donde ScriptName es el nombre del script. Para crear un registro script, agregue un registro a FOXCODE con "S" en TYPE, el nombre del script en ABBREV, "{}" en CMD, y el código a ejecutar en DATA. Utilizar un registro script tiene la ventaja de que es más compacto que un editor de propiedad separado (ya que el código está contenido en FOXCODE) y no son necesarias ningunas rutas.

Al igual que un generador, un editor de propiedad es responsable de obtener una referencia al objeto que será modificado y de escribir a la propiedad del objeto. De hecho, puede pensar que un editor de propiedad como un subconjunto de un generador (el que es usualmente un editor para múltiples propiedades de un objeto), con requerimientos y características de arquitectura similares.

Llegar hasta allá desde aquí

Ahora que sabe como trabaja MemberData, puede utilizarlo agregando una propiedad _MemberData a cada objeto y rellenando luego el XML adecuado de esta propiedad. ¿No parece que esto vaya a reducir su productividad en lugar de aumentarla? Afortunadamente, hay dos cosas que podemos hacer para decirle a VFP que cree automáticamente una propiedad _MemberData para cada clase y genere automáticamente el XML que necesitamos.

La clave para esta primera tarea es agregar un registro a la tabla IntelliSense que se ejecuta cada vez que abra la clase en el diseñador de clases o en un formulario en el Diseñador de formularios. Esto provoca que cuando la ventana Propiedades muestra las PEMs para un objeto, VFP busca en la tabla IntelliSense un registro con TYPE igual a "E" y ABBREV igual a "_GetMemberData". Si encuentra este tipo de registro, ejecutará el código que aparece en el campo memo DATA. Entonces, podemos agregar un registro a FOXCODE que cree una propiedad _MemberData en todas las clases o formularios que no la tengan ya. He aquí un código para hacerlo:

local lcCode
* Crear el código que se desea inserter en FOXCODE.
text to lcCode noshow
lparameters toFoxcode
local laObjects[1], ;
loObject
if aselobj(laObjects) = 0 and aselobj(laObjects, 1) = 0
return ''
endif aselobj(laObjects) = 0 ...
loObject = laObjects[1]
if vartype(loObject) = 'O' and ;
not pemstatus(loObject, '_memberdata', 5)
loObject.AddProperty('_memberdata', '')
endif vartype(loObject) <> = 'O' ...
return ''
endtext
* Crear ahora un registro en FOXCODE que se ejecute
* siempre que se abra una clase en el Diseñador de clases.
use (_foxcode) again shared alias FOXCODE
locate for TYPE = 'E' and ABBREV = '_GetMemberData'
if not found()
insert into FOXCODE (TYPE, ABBREV, DATA) values ('E', ;
'_GetMemberData', lcCode)
endif not found()
use

Este código verifica si el registro _GetMemberData ya existe o no. Al momento de escribir este artículo, Microsoft no ha decidido si este registro va a formar parte, o no, del conjunto estándar de registros en FOXCODE.

El código insertado al campo memo DATA utiliza ASELOBJ() para obtener una referencia a una clase o formulario que será editado, verifica la existencia de _MemberData con PEMSTATUS(), y agrega la propiedad utilizando AddProperty si no ha sido encontrada.

Para ver cómo funciona, ejecute el código (UpdateFoxCode.PRG en el archivo Descargar), y luego escriba lo siguiente en la ventana Comandos:

create class x of x as custom

En la ventana Propiedades verá que VFP automáticamente creó una propiedad _MemberData para esta clase.
Ahora tenemos una propiedad _MemberData para cada cosa que abramos en el Diseñador de clases o Diseñador de formularios, ¿Cómo pudiéramos abordar la segunda tarea: generar el XML desde el metadato?

El editor de la propiedad _MemberData

Esta tarea es muy sencilla de cumplir debido a que VFP 9 incluye una nueva herramienta llamada Editor de MemberData (MemberData Editor) (vea Figura 4). Esta herramienta permite especificar visualmente los atributos para PEMs en un objeto. Entonces se encarga de generar el XML adecuado para la propiedad _MemberData.

Existen varias formas de invocar al Editor de MemberData:

  • El Editor MemberData puede ser registrado globalmente como editor de propiedad para la propiedad _MemberData ejecutando simplemente (DO HOME() + 'MemberDataEditor.APP'). Después de hacer esto, puede seleccionar la propiedad _MemberData en la ventana Propiedades y hacer Clic en el botón editor de propiedad para llamar al Editor de MemberData.
  • Los menús Formularios y Clases tienen una nueva opción “Editor de MemberData” ("MemberData Editor") que invoca al editor.
  • Puede invocar al Editor de MemberData mediante programación con DO HOME() + 'MemberDataEditor.APP'. Si es seleccionado un objeto en el Diseñador de clases o Diseñador de Formularios, se abrirá el editor.
  • Si desea simplemente agregar un PEM a la ficha Favoritos de la ventana Propiedades, haga Clic derecho en la PEM y seleccione la nueva opción “Agregar a Favoritos” ("Add to Favorites"). Esta opción llama en silencio al Editor de MemberData, el que actualiza el XML en _MemberData sin mostrar ninguna interfaz de usuario. Observe que no hay ninguna opción “Quitar de Favoritos” ("Remove from Favorites"); para lograrlo, es necesario utilizar la interfaz del Editor de MemberData Editor.

Para probar esto, cree o modifique una clase en el Diseñador de clases. Agregue algunas propiedades y métodos de usuario, y seleccione luego " Editor de MemberData " desde el menú Clase. Modifique los atributos de MemberData de las propiedades de usuario para que muestren las letras en mayúsculas, y coloque algunas en la ficha favoritos. Haga clic en el botón Aceptar (OK) y observe que los nuevos miembros aparecen ahora en la forma que desea en la ventana Favoritos.

Resumen

MemberData es una gran mejora de productividad en VFP 9 debido a que nos permite especificar cómo deben ser mostrados los miembros en la ventana Propiedades y en IntelliSense, haciendo más fácil encontrar (y ahorrarse la edición si permite a IntelliSense que inserte sus nombres en el código). Además, como los generadores, los editores de propiedades pueden facilitar, la asignación del valor adecuado a la propiedad. Espero que veamos muchos editores de propiedades con VFP 9, y muchos otros se irán creando por emprendedores desarrolladores de VFP.

Descarga

Archivo de descarga: memberdata.zip