Un generatore di report in ASP |
| A cura di Davide Bianchi |
Un generatore di report che consente di produrre stampe di tutti
(o quasi) i generi direttamente in ASP, senza la necessita' di
installare componenti aggiuntivi sul server.
|
| La Stampa, che pena... |
Ok, abbiamo fatto il lavoro in ASP, c'e' tutto, o quasi. Il
database funziona come un orologio svizzero (no, non fa cucu). I
dati possono essere visionati in 42 modi diversi, le tabelle vengono
preparate, abbiamo perfino scovato il maledetto Applet Java per
visualizzare i grafici come piace a quel xyz del cliente,
abbiamo tutto.... ma le stampe? Ebbene si', ammettiamolo: le stampe fatte dal browser fanno veramente schifo! Non c'e' modo di controllare se la stampa esce in Orizzontale o in Verticale, va' a capo quando vuole lui, spezza le tabelle a meta' che fa paura, non si riesce ad impostargli niente! La verita' e' che il browser non e' stato pensato per stampare, e le stampe che si riesce ad ottenre dal browser "nudo e crudo" fanno veramente pieta'. Certo, si puo' intervenire installando dei componenti server-side che consentono di produrre delle bellissime stampe, Crystal Report o Business Object producono dei risultati ottimi in quanto a questo, ma.... costano cari ed inoltre occorre installare le versioni Server sul server e distribuire gli applicativi client. E sappiamo gia' cosa ci rispondera' il gestore del sito alla domanda "possiamo installare sta' roba?". Dopo una serie di (dis)avventure con i succitati prodotti, mi sono deciso finalmente a produrre qualche cosa che possa risolvere il problema. Quello che volevo IO era un prodotto/meccanismo che:
Na' roba da ridere insomma (dira' qualcuno).
|
||||||||||||||||||||||||||||||
| Produrre un documento Word |
La prima cosa che mi e' venuta in mente quanto ho messo le mani sul
problema e' stata la produzione di un documento Word sul server
da "rigirare" poi sul client. Word e' un OLE Server, questo vuole dire che posso "generare" un documento Word esattamente come "attivo" una connessione ad un database, usando CreateObject(), quindi usare i metodi dell'oggetto Word per "pilotare" il documento stesso. Questa soluzione comporta pero' alcuni problemi, innanzitutto Word deve essere installato sul server, cosa che non sempre e' possibile, secondariamente i "metodi" di Word non sono la cosa piu' semplice del mondo da usare. Questi due problemi, uniti al fatto che dopo mezz'ora di prove avevo 18 (!) sessioni di Word "zombie" sul server mi hanno fatto rapidamente scartare Word come soluzione valida.
|
||||||||||||||||||||||||||||||
| Un documento RTF |
L'esperienza Word pero' mi ha fatto pensare un momento: cosa posso
produrre che sia "visto" come un documento con tali caratteristiche ma
senza essere un documento Word? Risposta: un documento RTF. L'RTF e' un formato di documento "portabile" inventato da Microsoft all'inizio degli anni 90 per definire i testi che poi vengono trasformati nel sistema di help di Windows (.hlp). Un file RTF e' un file "ascii", cioe' scritto in modo da essere (piu' o meno) leggibile direttamente, ma contiene dei tag, cioe' degli indicatori che permottono di specificare come impostare il carattere, il paragrafo, la pagina etc. etc. Diciamo che e' un livello piu' "sofisticato" di HTML, dove oltre a specificare la formattazione del carattere e del paragrafo, possiamo specificare anche quella della pagina, quindi forzare una dimension/orientamento per la stessa, forzare i salti pagina e cosi' via. La cosa che interessa pero' e' che e' un testo ASCII, cioe' puo' essere scritto direttamente senza l'ausilio di nessun programma specifico, esattamente come un buon WebMaster puo' scrivere un documento HTML senza dover usare nessun editor particolare. Nota: per ulteriori informazioni sull'RTF vedere il relativo documento, oppure vedere la documentazione MSDN direttamente sul sito della Microsoft. Per produrre il documento RTF posso utilizzare il FileSystemObject, quindi mi basta redirigere l'utente sul documento RTF prodotto perche' questo venga visualizzato nel browser utilizzando il visualizzatore predefinito, se l'utente ha installato Word verra' aperto Word, altrimenti il browser scegliera' quello che e' disponibile al momento (anche il mefitico WordPad e' in grado di interpretare l'RTF).
|
||||||||||||||||||||||||||||||
| Lo scheletro del report |
Generare un file RTF e' un po' come generare un documento HTML, solo
che i tag RTF sono molti, ma molti di piu'. Ho quindi scartato a priori
l'idea di scriverlo direttamente nel codice (usando una sfilza
di Write("....") insomma). Ci vuole qualche cosa di piu' sofisticato. Il mio "generatore" sara' quindi costituito da una serie di funzioni, il cui scopo e' quello di "incapsulare" la generazione del documento finale in una serie di procedure molto semplici e, soprattutto, riutilizzabili. Ho bisogno poi dei Tag RTF che mi servono, siccome sono tanti preferisco averne un elenco e leggerli di volta in volta quando mi servono, in questo modo se voglio aggiungere un tag devo solo aggiungerlo all'elenco, il codice e' predisposto per cercarlo nell'elenco e lo trovera'. La funzione "principale" e' quella che preleva dall'elenco di tag quello richiesto al momento e lo "spara" nel file che stiamo costruendo. Il meccanismo quindi si presenta cosi':
(1) devo scrivere un tag cerco il tag nell'elenco scrivo il tag ritorno a (1)Per comodita' poi ho pensato che sarebbe utile avere due funzioni di "scrittura" sul file: una che scrive senza andare a capo, e l'altra che invece interrompe la linea e porta il testo a capo (equivalente ad inserire un insomma). Dopo un po' di elucubrazioni sono arrivato al seguente elenco "minimo" di funzioni da implementare:
Dopo molti tentennamenti ho deciso di lasciare perdere (per il momento) la gestione delle vere e proprie tabelle in RTF perche' sono un macello inimmaginabile, inoltre se Word incontra una tabella fatta non proprio come si deve va' irrimediabilmente in crash. Ognuna delle funzioni sopra descritte, ha la stessa struttura:
Quindi per prima cosa mi serve la funzione che legge dall'elenco il TAG che voglio. Per evitare di complicare le cose ho deciso di non utilizzare un database per memorizzare i tag, troppo complicato e fondamentalmente inutile, i Tag li memorizzo in un file ASCII, avente tale struttura:
Il "@" separa il codice dal tag vero e proprio, questo significa che ne' il tag, ne' il codice possono contenere il carattere "@", in quanto a questo siamo a posto perche' nessun tag RTF contiene il "@".
|
||||||||||||||||||||||||||||||
| Recuperare un Tag dall'elenco |
La funzione che legge un TAG dall'elenco dei tag e' relativamente
semplice, utilizziamo il FileSystemObject per accedere al file in
modalita' Ascii e leggere una linea alla volta, ogni linea viene
poi "spezzata" in codice e tag, quindi confrontiamo il codice con
quello che stiamo cercando, se il codice corrisponde la funzione
ritorna il corrispondente Tag.
/*
* Cerca un tag specificato all'interno del file dei TAG
* Ritorna il tag cercato o NULL se non trovato.
*/
function LeggiTag( TagID, fileName )
{
var fso; // FileSystemObject
var file; // Fiile dei tag
var elem; // elementi del tag
var tag = null; // tag da ritornare
var ro = / /g; // Usata per 'magniare' gli spazi
var i=1;
// Creo un nuovo oggetto FileSystem
fso=new ActiveXObject( "Scripting.FileSystemObject" );
// accedo al file dei tag
// N.B. non verifico se il file esiste...
// mica troppo bello
file = fso.OpenTextFile( fileName );
// elimino gli spazi all'interno del codice
TagID = TagID.replace( ro, "" );
// ciclo e leggo tutti i Tag
while( ! file.AtEndOfStream )
{
// leggo il singolo Tag
tag = file.ReadLine();
// parserizzo suddividendo in Codice e Testo ed
// elimino gli spazi
elem = SeparaTag( tag, null );
elem[0] = elem[0].replace( ro, "" );
// verifico di aver trovato il tag che cercavo
tag="";
if( TagID == elem[0] ) {
// ritorno il tag
for( i=1; i < elem.length; i++ )
tag += elem[i];
break;
}
}
// chiudo il file
file.close();
// ritorno il tag trovato o Null
return tag;
}
Questa funzione non e' proprio "il massimo", in quanto non verifico
se il file esiste o no, mentre sarebbe bene verificarlo sempre.L'unica cosa "strana" e' l'utilizzo di una RegularExpression per l'eliminazione degli spazi. La funzione utilizza SeparaTag() per separare il tag letto nei suoi componenti (Codice e Tag), questa funzione e' definita come segue:
/*
SeparaTag
Separa un Tag letto dal file nei suoi elementi
un codice ed una stringa che vengono ritornati in un array
costruito per l'occasione.
Parametri:
tag e' il tag letto dal file
sep e' il carattere usato per dividere i due pezzi
*/
function SeparaTag( tag, sep )
{
var ro;
// se il separatore non e' specificato assumo "@"
if( sep == null ) {
ro = new RegExp( "@", "g" );
} else {
ro = new RegExp( sep, "g" );
}
return tag.split( ro );
}
L'utilizzo delle Regular Expression per effettuare il lavoro
semplifica di molto la funzione stessa, in pratica il tutto si
riduce ad una linea di codice.L'ultima funzione "generica" che mi serve e' la seguente: recupera un Tag dal file usando le due funzioni precedenti e lo scrive dentro nel file del report indicato.
/*
* Recupera un tag e lo scrive nel report
* Funzione che viene usato dappertutto
*
* tagFile e' il nome del file dei tag da usare
* reportFile e' un File gia' aperto.
* tag e' l'ID del tag da inserire
*
*/
function ScriviTagNelReport( tagFile, reportFile, tag )
{
var tmpTag;
// recupero il tag di inizializzazione e lo scrivo nel file
tmpTag = LeggiTag( tag, tagFile );
reportFile.WriteLine( tmpTag );
}
|
||||||||||||||||||||||||||||||
| Iniziare e finire il Report |
Adesso che abbiamo le funzioni "basilari" del sistema possiamo
cominciare dall'inizio...e dalla fine: aprire e chiudere il report. Un documento RTF, come un documento HTML deve essere "aperto" e "chiuso" con i tag corretti, questi tag sono scritti nell'elenco dei tag e vengono recuperati dalle funzioni che io ho definito come IniziaReport() e ChiudiReport(), che a loro volta sono definite come segue:
/*
* Inizializza il report
* Crea il file denominato "reportFile" e scrive
* i tag di inizializzazione dell'RTF. I tag sono
* letti dal file indicato come "tagFile"
*
* Ritorna il nuovo oggetto File creato.
*/
function IniziaReport( reportFile, tagFile )
{
var fso; // FileSystemObject
var fileReport; // file del Report
// creo l'oggetto FSO
fso = new ActiveXObject("Scripting.FileSystemObject");
// creo il file del report
fileReport = fso.CreateTextFile( reportFile );
// recupero il tag di inizializzazione e lo scrivo nel file
ScriviTagNelReport( tagFile, fileReport, "InizializzaReport" );
// ritorno il report appena creato
return fileReport;
}
/*
* Termina il report
* Scrive nel report indicato i tag di chiusura dell'RTF e
* chiude il file.
*/
function ChiudiReport( reportFile, tagFile )
{
// recupero il tag di chiusura e lo scrivo nel file
ScriviTagNelReport( tagFile, reportFile, "TerminaReport" );
// chiudo il file
reportFile.Close();
}
|
||||||||||||||||||||||||||||||
| Impostare il formato della pagina |
Il formato della pagina puo' essere impostato come "Portrait" o
"Landscape", si tratta solo di inserire una sequenza o l'altra
nel report.
/*
* Imposta il formato pagina a Portrait
*/
function ImpostaPortrait( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "Portrait" );
}
/*
* Imposta il formato pagina a Landscape
*/
function ImpostaLandscape( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "Landscape" );
}
Come si puo' vedere, una volta definite le funzioni "basilari", le
altre funzioni sono di una banalita' assurda.
|
||||||||||||||||||||||||||||||
| Scrivere linee e paragrafi |
La scrittura di linee e paragrafi richiede il passaggio del testo
da scrivere, quindi la funzione e' leggermente diversa dalle
precedenti, ma non di molto.
/*
* Scrive una linea senza mettere una fine paragrafo
* testo e' il testo da scrivere
*
* N.B. 'tagFile' non e' usato, ma viene richiesto per
* mantenere una certa coerenza con tutte le altre funzioni.
*/
function ScriviLinea( reportFile, tagFile, testo )
{
reportFile.Write( testo + " ");
}
/*
* Scrive una linea aggiungendo la fine del paragrafo
*/
function ScriviLineaACapo( reportFile, tagFile, testo )
{
reportFile.Write( testo );
ChiudiParagrafo( reportFile, tagFile );
}
/*
* Inserisce la fine del paragrafo
*/
function ChiudiParagrafo( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "FineParagrafo" );
}
|
||||||||||||||||||||||||||||||
| Definire e scegliere uno stile di testo |
Per definire e poi scegliere uno stile occorre inserire due tag
distinti: per prima cosa occorre definire lo stile creando una apposita
"tabella di stili", poi si utilizza il tag di selezione per
scegliere lo stile da usare nel paragrafo. Siccome la tabella degli stili e' quasi sempre la stessa, ho deciso di usare una tabella "fissa" definendo in anticipo tutti gli stili che mi servono ed "infilandoli" per default nel mio report, poi decidero quale stile usare di volta in volta. Per selezionare uno stile mi basta riferirmi allo stile della tabella ma specificando la dimensione del font. Per fare questo devo raddoppiare la definizione degli stili, inserendo la prima volta lo stile nella tabella e la seconda volta lo stile con la dimensione del carattere. Il codice che esegue la gestione degli stili e' molto semplice:
/*
* Inserisce l'inizio della Tabella degli Stili
*/
function IniziaTabellaStili( reportFile, tagFile )
{
ScriviTagNelReport( tagFile,
reportFile, "InizioTabellaStili" );
}
/*
* Inserisce lo stile nella tabella
*/
function InserisciStile( reportFile, tagFile, stile )
{
ScriviTagNelReport( tagFile, reportFile, stile )
}
/*
* Chiude la tabella stili
*/
function ChiudiTabellaStili( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "FineTabellaStili" );
}
/*
* Inserisce la tabella degli stili 'default'
*/
function InserisciTabellaStili( reportFile, tagFile )
{
// inizializzo la tabella
IniziaTabellaStili( reportFile, tagFile );
// inserisco gli stili
InserisciStile( reportFile, tagFile, "Titolo1" );
InserisciStile( reportFile, tagFile, "Titolo2" );
InserisciStile( reportFile, tagFile, "Titolo3" );
InserisciStile( reportFile, tagFile, "Titolo4" );
InserisciStile( reportFile, tagFile, "Normale" );
InserisciStile( reportFile, tagFile, "Testo" );
InserisciStile( reportFile, tagFile, "PieDiPagina" );
InserisciStile( reportFile, tagFile, "Intestazione" );
// chiudo la tabella degli stili
ChiudiTabellaStili( reportFile, tagFile );
}
Per "attivare" uno stile occorre pescare dall'elenco il tag "corretto":
/*
* Inizia a scrivere con uno stile
*/
function IniziaStile( reportFile, tagFile, stile )
{
ScriviTagNelReport( tagFile, reportFile, "Stile_" + stile );
}
/*
* Termina lo stile corrente
*/
function ChiudiStile( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "FineStile" );
}
Il trucco consiste nel fatto che per ogni stile ci sono due "tag"
nel nostro file dei tag, per lo stile "Normale" abbiamo il tag
definito come "Normale" e quello "Stile_Normale", in cui viene
richiamato il primo con l'aggiunta della dimensione del carattere:
Normale@{\f5\froman\fcharset0\fprq2 Times New Roman;}
Stile_Normale@{\f5\fs20
|
||||||||||||||||||||||||||||||
| Grassetto e corsivo |
La gestione del Grassetto e del Corsivo (Italico), e' paurosamente
semplice a questo punto: basta inserire i due tag necessari:
/*
* Mette il testo in grassetto
*/
function AttivaGrassetto( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "InizioGrassetto" );
}
/*
* Mette il testo in corsivo
*/
function AttivaCorsivo( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "InizioCorsivo" );
}
/*
* Disattiva il grassetto
*/
function FineGrassetto( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "FineGrassetto" );
}
/*
* Disattiva il corsivo
*/
function FineCorsivo( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "FineCorsivo" );
}
|
||||||||||||||||||||||||||||||
| Le Tabulazioni (tabelle) |
Ho deciso di lasciare perdere le vere e proprie Tabelle RTF in
quanto non tutti i Viewer le supportano e non tutti nello stesso
modo, quindi utilizzero' solamente dei tabulatori per costruire
delle visualizzazioni tabellari. La definizione di una tabulazione in RTF consiste fondamentalmente nell'inserire un tag che specifica dove deve trovarsi la tabulazione, ed un tag per usare la tabulazione stessa. In RTF le tabulazione devono essere specificate in Twips (ventesimi di punto tipografico), per evitare problemi pero' preferisco convertira i dati internamente, permettendo cosi' di specificare il tutto in millimetri.
/*
* Imposta una tabulazione
*/
function ImpostaTabulazione( reportFile, tagFile, tabulazione )
{
var tabTwips;
var tag;
tabTwips = tabulazione * 56.7
tag = LeggiTag( "Tabulazione", tagFile ) + tabTwips + " ";
reportFile.WriteLine( tag );
}
/*
* Inizia una linea tabulata
*/
function IniziaLinea( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "InitTab" );
}
/*
* Chiude una linea tabulata
*/
function FinisciLinea( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "EndTab" );
}
/*
* Inserisce un carattere di tabulazione
*/
function InserisiciTab( reportFile, tagFile )
{
ScriviTagNelReport( tagFile, reportFile, "Tab" );
}
La ImpostaTabulazione
e' una delle poche funzioni in cui non viene usata la
ScriviTagNelReport
ma si usa la WriteLine
direttamente.Le IniziaLinea e FinisciLinea sono necessarie in quanto ogni linea deve essere raggruppata all'interno di un blocco definito da parentesi.
|
||||||||||||||||||||||||||||||
| L'elenco dei Tag |
Fino ad ora abbiamo definito le funzioni che leggono i vari Tag e li
inseriscono nel report, ora dobbiamo definire i singoli Tag. L'elenco dei Tag e' il seguente:
InizializzaReport@{\rtf1\ansi
MarginiPagina@\margl1134\margr1134\margt1418\margb1134
TerminaReport@}
Portrait@\paperw11907\paperh16840
Landscape@\paperw16840\paperh11907
FineParagrafo@\par
InizioTabellaStili@{\fonttbl
Titolo1@{\f1\fswiss\fcharset0\fprq2 Arial;}
Titolo2@{\f2\fswiss\fcharset0\fprq2 Arial;}
Titolo3@{\f3\fswiss\fcharset0\fprq2 Arial;}
Titolo4@{\f4\froman\fcharset0\fprq2 Times New Roman;}
Normale@{\f5\froman\fcharset0\fprq2 Times New Roman;}
Testo@{\f6\froman\fcharset0\fprq2 Courier;}
PieDiPagina@{\f6\froman\fcharset0\fprq2 Courier;}
Intestazione@{\f6\froman\fcharset0\fprq2 Courier;}
FineTabellaStili@}
Stile_Titolo1@{\f1\fs48
Stile_Titolo2@{\f2\fs40
Stile_Titolo3@{\f3\fs36
Stile_Titolo4@{\f4\fs24\b
Stile_Normale@{\f5\fs20
Stile_Testo@{\f6\fs20
Stile_PieDiPagina@{\f7\fs16
Stile_Intestazione@{\f8\fs16
FineStile@}
InizioGrassetto@{\b
FineGrassetto@}
InizioCorsivo@{\i
FineCorsivo@}
Tabulazione@\tx
Tab@\tab
InitTab@{
EndTab@}\par
Il Tag MarginiPagina e'
predisposto per dimensionare la pagina come un A4 (21x29.7 cm), se
si volesse usare una dimensione di pagina diversa bastera' modificare
la dimensione o aggiungere altri Tag "specializzati" per
le varie dimensioni. I margini sono specificati in Twips.
|
||||||||||||||||||||||||||||||
| Usare il generatore |
Per utilizzare il generatore e' sufficiente includere il codice ASP
dello stesso, quindi richiamare le singole funzioni. Un esempio di
generazione di un report molto semplice e' il seguente:
Dim fileTag
Dim fileReport
' apro una connessione ad un database e creo un recordset
' ---- OMISSIS
' inizializzo il report
fileTag = Server.MapPath("Tag.Lst")
report = Server.MapPath("report.rtf")
Set fileReport = IniziaReport(report, fileTag )
ImpostaLandscape fileReport, fileTag
InserisciTabellaStili fileReport, fileTag
IniziaStile fileReport, fileTag, "Titolo1"
ScriviLineaACapo fileReport, fileTag, "Report di verifica"
ChiudiStile fileReport, fileTag
IniziaStile fileReport, fileTag, "Normale"
' imposto le tabulazioni
ImpostaTabulazione fileReport, fileTag, 20
ImpostaTabulazione fileReport, fileTag, 40
ImpostaTabulazione fileReport, fileTag, 60
ImpostaTabulazione fileReport, fileTag, 80
IniziaLinea fileReport, fileTag
' visualizzo i titoli
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, "colonna 1"
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, "colonna 2"
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, "colonna 3"
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, "colonna 4"
FinisciLinea fileReport, fileTag
ScriviLineaACapo fileReport, fileTag, ""
' ciclo e stampo tutti i dati
Do While Not recordset.Eof
IniziaLinea fileReport, fileTag
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, recordset.Fields("primo")
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, recordset.Fields("secondo")
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, recordset.Fields("terzo")
InserisiciTab fileReport, fileTag
ScriviLinea fileReport, fileTag, recordset.Fields("quarto")
FinisciLinea fileReport, fileTag
recordset.MoveNext
Loop
' eventuali totali etc.
|
||||||||||||||||||||||||||||||
| Il codice completo |
Il codice del generatore e questo articolo sono
nell'Archivio.
|
|
Comments Max length of comments: 1000 chars. |
1 commento stecolna dice il 21/10/2008 23:48: Ma certe genialate come ti vengono?? Ce lo puoi spiegare?? necessita' Add a comment (max 1000 chars)
|
| Author |
Davide Bianchi,
works as Unix/Linux administrator for a "network security" company of Haarlem. Contacts: mail: davide AT onlyforfun.net , ICQ: 268751033, Jabber: davideyeahsure AT gmail.com Skype: davideyahsure |
| Contribuire | Volete contribuire? Leggete come! |
| Copyright | This site is made by me with blood, sweat and gunpowder, if you want to republish or redistribute any part of it, please drop me (or the author of the article if is not me) a mail. |
This site isn't optimized for vision with any specific browser, nor
it requires special fonts or resolution.
You're free to see it as you wish.
Last Update: 04/12/2008