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.
Evento | Descripción |
---|
BeforeReport | Se 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.
Propiedad | Tipo | Descripció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 | N | Porción roja del valor RGB() para el color de relleno |
FontName | C | Nombre de la fuente |
FontSize | N | Tamaño de la fuente |
FontStyle | N | Un valor que representa el estilo de fuente. Los valores son: 1 (negrita), 2 (itálica), 4(subrayada) y 128 (tachada) |
PenAlpha | N | Color del pincel (alpha) |
PenBlue | N | Porción azul del valor RGB() para el color de pincel |
PenGreen | N | Porción verde del valor RGB() para el color de pincel |
PenRed | N | Porción roja del valor RGB() para el color de pincel |
Reload | L | Iguale a .T. para notificar al motor de informe que ha modificado una o más propiedades. |
Text | C | Texto a mostrar para el objeto campo. |
Value | Varios | Valor 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.