Utilizzare il designer DataReport di Visual Basic 6 (seconda parte)
a cura di Amedeo Fantini (requisiti: conoscenza generica di VB)

Associazione manuale dei dati
Nella conclusione della prima parte si affermava come l'associazione ai dati tramite un DataEnvironment non fosse l'unica soluzione. I dati possono venir associati anche manualmente.
Il primo passo per gestire manualmente l’associazione dei dati al Data Report, oltre a lasciare vuote le proprietà DataSource e DataMember, è quello d’identificare un punto d’entrata destinato a contenere il codice necessario alla creazione del Command, e delle corrette associazioni ai controlli.
L’evento Initialize sarebbe intuitivamente il posto più indicato per concentrare il codice, ma esso non permette il passaggio di parametri, presupposto basilare per poter mantenere un controllo adeguato sul funzionamento della procedura.
Inoltre una volta eseguito il metodo Show / PrintReport, che scatenano l’evento Initialize, non si può più tornare indietro, ed in caso d’errore si sarebbe costretti comunque a visualizzare la finestra del report o a stampare una pagina vuota.
Per risolvere il problema occorre quindi aggiungere un proprio metodo al Data Report, per ricevere e restituire parametri, dentro al quale concentrare il codice ed eventualmente avvisare l’utente se l’elaborazione ha avuto esito positivo, o viceversa.

'Da aggiungere al modulo del DataReport
Public Function InitReport(Param1 as String, Param2 as Long) as Boolean
....
End Function

...quindi dal form chiamante:
Private Sub RunReport()
  If DataReport1.InitReport(pString, pLong) = True Then
    DataReport1.Show vbModal
  Else
    MsgBox “Impossibile eseguire la stampa”
  Endif
End Sub

Naturalmente la chiamata del metodo InitReport scatena l’evento Initialize, dentro al quale potete benissimo lasciare il codice necessario al dimensionamento della pagina e del form che conterrà la stampa.
Nell’esempio seguente, oltre alle impostazioni relative alla dimensione, si carica anche un logo da stampare:

Private Sub DataReport_Initialize()

    'Imposta margini e larghezza del report
    With Me
        .TopMargin = 0
        .BottomMargin = 800
        .LeftMargin = 0
        .RightMargin = 0
        .ReportWidth = 11340

        .Width = (Screen.Width / 10) * 9
        .Height = (Screen.Height / 10) * 9

        ' Centra il form che conterrà l’anteprima
        .Left = (Screen.Width - .Width) / 2
        .Top = (Screen.Height - .Height) / 2


        With .Sections("PageHeader")
            'Imposta le coordinate e la dimensione del logo
            With .Controls("imgLogo")
                .Width = 3232
                .Height = 964
                .Left = 3742
                .Top = 0
                .SizeMode = vbOLESizeStretch
         ‘Carica il logo dal file risorse legato al progetto
                Set .Picture = LoadResPicture("LOGOSTAMPA", vbResBitmap)
            End With

            'Imposta il titolo del report
            With .Controls("lblTitoloReport")
                .Left = 0
                .Top = 510
         .Caption = “Questo è il titolo della stampa”
            End With
      End With
    End With

End Sub

Una volta ottenuto il recordset da associare al Data Report, sia esso derivante dall’esecuzione di un command contenuto nel DataEnvironment, o creato manualmente, occorre agganciarlo fisicamente al Data Report stesso con il seguente comando:

Set DataReport1.DataSource = myRecordset

Ricordo che, utilizzando il sistema d’associazione manuale, occorre sempre specificare nei singoli campi contenuti nelle sezioni del report, a quale campo ed a quale recordset devono essere associati.

Intervenire durante l’esecuzione della stampa
Nella folta schiera dei programmatori ci sono coloro che adorano programmare cercando di interagire il meno possibile con il processo di esecuzione, e quelli che invece desiderano avere sotto controllo ogni singolo step d’esecuzione, i quali quindi necessitano d’impostare tutto manualmente e diventano delle "iene" quando questo non gli viene concesso.
Dal momento che io faccio parte della seconda categoria, devo confessare che il Data Report mi fa soffrire perché mi lascia ben poco spazio per poter intervenire durante la sua esecuzione.
Fortunatamente esiste una piccola porta attraverso la quale è possibile entrare durante l’esecuzione, e partecipare più o meno attivamente alla creazione della stampa.
Questa porta è resa disponibile dall’oggetto StdDataFormat, attraverso il quale possiamo intervenire direttamente nella fase di stampa, e di conseguenza modificare o decodificare dati legati a tabelle o strutture esterne.

Option Explicit

Dim WithEvents myStdData As StdDataFormat

Private Sub DataReport_Initialize()
  Set myStdData = New StdDataFormat
  Set Me.Sections(“Dettaglio”).Controls("Text1").DataFormat = myStdData
End Sub

Private Sub myStdData_Format(ByVal DataValue As StdFormat.StdDataValue)
  DataValue.Value = ".Eureka!!"
End Sub

Per utilizzare questa tecnica occorre fare alcune considerazioni:

  1. l’evento legato al controllo StdDataFormat scatta soltanto se il controllo al quale è stato associato è visibile all’interno della sezione (proprietà Visible=True);
  2. all’interno dell’evento Format non è possibile accedere al contenuto di un campo del recordset, perché il riferimento alla proprietà .Fields ottenibile all’interno dell’evento stesso, restituisce sempre il contenuto del primo record e non di quello sul quale è posizionato il flusso del Data Report;
  3. gli eventi Format associati ai controlli (naturalmente quando se ne utilizza più di uno) scattano in sequenza all’interno della stessa sezione, ordinati in base alla relativa posizione "n" dell’oggetto Fields nel recordset;

Nell’esempio precedente è stato dichiarato un oggetto StdDataFormat, in modo tale da rispondere agli eventi, con la parola chiave WithEvents. Di conseguenza ogni qualvolta il contenuto del campo associato viene prelevato dal recordset, scatta l’evento "Format" all’interno del quale possiamo scrivere il codice per effettuare le modifiche del caso.
Purtroppo non è possibile intervenire sul contenuto di un altro controllo presente nella stessa sezione, riferendosi semplicemente all'oggetto Controls, proprio a causa delle considerazioni fatte nel paragrafo "I componenti" (fortunatamente il problema si risolve in un altro modo).

Immaginiamo allora di avere sul recordset un campo che identifica la tipologia di un pagamento, la cui codifica non è inserita in una tabella nel Database, ma in una variabile di enumerazione dichiarata nel codice:

Enum TipoPagamento
  Assegno=1
  Bonifico =2
  Contante = 3
End Enum

In questo caso non è chiaramente possibile inserire la decodifica del pagamento all’interno del recordset (tranne nel caso in cui il recordset venga creato manualmente), e quindi occorre necessariamente effettuarla durante l’esecuzione della stampa nel seguente modo:

Private Sub myStdData_Format(ByVal DataValue As StdFormat.StdDataValue)

    Select Case DataValue.Value
        Case TipoPagamento.Assegno
            DataValue.Value = "Assegno"
        Case TipoPagamento.Bonifico
            DataValue.Value = "Bonifico bancario"
        Case TipoPagamento.Contante
            DataValue.Value = "Contante"
    End Select

End Sub

L’esempio resta valido anche nel caso (più complesso) in cui sia necessario prelevare la decodifica da una tabella presente nel Database.

Per inizializzare il contenuto di un controllo presente nella stessa sezione con la descrizionedel pagamento, occorre utilizzare un particolare stratagemma: aggiungere un controllo rptTextBox (che conterrà la descrizione del pagamento) e legarlo ad un campo del recordset il cui indice è successivo a quello relativo al codice di pagamento (vedi punto 3 precedente). Dichiarare poi una variabile visibile in tutto il modulo, che verrà inizializzata nell'evento format legato al controllo "codice pagamento", che servirà da inizializzare la proprietà DataValue.Value nell'evento Format legato al controllo rptTextBox.

'supponiamo di avere inserito un controllo stdDataFormat "myStdDesPagamento" e
'di averlo legato al controllo rptTextBox "txtDesPagamento"

'--------------------------------------------------------------------------
' Evento che scatta quando viene prelevato dal recordset il codice Pagamento
'--------------------------------------------------------------------------
Private Sub myStdData_Format(ByVal DataValue As StdFormat.StdDataValue)

  Select Case DataValue.Value
    Case TipoPagamento.Assegno
      DataValue.Value = "Assegno"
      DesPagamento= "Assegno"
    Case TipoPagamento.Bonifico
      DataValue.Value = "Bonifico bancario"
      DesPagamento= "Bonifico bancario"
    Case TipoPagamento.Contante
      DataValue.Value = "Contante"
      DesPagamento= "Contante"
  End Select

End Sub


'--------------------------------------------------------------------------
' Evento che scatta quando viene prelevato dal recordset un campo successivo
' (indice "n") al codice del pagamento
'--------------------------------------------------------------------------
Private Sub myStdDesPagamento_Format(ByVal DataValue As StdFormat.StdDataValue)
   DataValue.Value = DesPagamento
End Sub

L’impiego di questa tecnica si rivela molto utile per superare alcuni limiti imposti dal Data Report; uno di questi è quello relativo al controllo rptFunction, con il quale è possibile effettuare somme/conteggi sui campi del recordset, senza però la possibilità di poter applicare un filtro con il quale decidere se e quando effettivamente considerare valido il dato elaborato.
Per ricollegarci all’esempio precedente supponiamo che al termine della stampa sia necessario suddividere gli importi degli assegni, dei bonifici e del contante.
Con l’impiego del controllo rptFunction possiamo soltanto sommare tutte le righe presenti nel recordset, ottenendo così il totale dei movimenti, e non riusciamo invece a suddividere l’importo per tipologia.
Attraverso la tecnica sopra esposta possiamo invece crearci un filtro da utilizzare proprio per raggiungere lo scopo, inserendo nella sezione "Piè di Pagina Report" un controllo rptLabel per ogni totale che desideriamo visualizzare.

Naturalmente il filtro possiamo applicarlo solo se conosciamo l’importo da sommare ed il tipo di movimento al quale si riferisce, dati che sono disponibili solo quando il Data Report li ha letti dal recordset e li ha associati al relativo controllo.

Il sistema migliore è quello di ampliare l'esempio precedente
Innanzitutto bisogna stabilire quale campo nel recordset è successivo all’ultimo che desideriamo valutare (vedi punto 3 precedente); questo ci serve per determinare il momento giusto nel quale effettuare il test di validità e l’inizializzazione dei totali (in alcuni casi può corrispondere all’ultimo campo contenente un valore di filtro).
Poi occorre inserire un controllo rptTextBox per ogni campo del recordset dal quale dobbiamo prelevare un valore necessario alla costruzione del filtro al quale associeremo un oggetto stdDataFormat.
Infine, nella gestione dell’evento Format per ognuno di questi, memorizziamo il valore contenuto nella proprietà DataValue.Value all’interno di una variabile visibile in tutto il modulo, ricordando di associare una stringa vuota alla proprietà stessa per pulirne il contenuto.
Così facendo quando scatterà l’evento Format relativo al campo scelto come riferimento, si potranno effettuare le considerazioni del caso ed inizializzare correttamente i controlli rptLabel dei totali con il comando DataReport1.Sections("Piè di Pagina Report").Controls(xx).Caption), qui permesso perchè ci riferiamo ad una sezione non ancora elaborata.

Stampare con recordset d’appoggio
Per poter risolvere alcune situazioni estreme è possibile impiegare alcuni accorgimenti non propriamente eleganti, ma altamente funzionali.
Il più semplice è quello di creare un recordset disconnesso dentro al quale si inseriscono i dati preformattati.
Per rendere meglio l’idea proviamo ad immaginare un vecchio file sequenziale dove ogni singolo record è identificato da un campo "tipo", con il quale possiamo decidere, intervenendo durante il flusso di stampa, di inizializzare in modo mirato i controlli inseriti all’interno del report.
In pratica si utilizza semplicemente il Data Report per inviare alla stampante quello che abbiamo già caricato in modo mirato all’interno di un file.

Dim rsAppoggio as New ADODB.RecordSet
With rsAppoggio
  .Fields.Append "TipoRiga", adChar, 1
  .Fields.Append "DesRiga", adChar, 100
  .Fields.Append "QuantitaRiga", adChar, 3
  .Fields.Append "PrezzoRiga", adDouble
  .Fields.Append "ImportoRiga", adDouble
  .Fields.Append "UnitaMisura", adChar, 2
  .Open
End With

'-------------------------------------------------------------------------------
' Aggiunge una riga nel recordset di supporto
'-------------------------------------------------------------------------------
Private Sub AggiungiRigaAppoggio(pTipoRiga As String, _
                                                   pDesRiga As String, _
                                                   pQuantita As Double, _
                                                   pPrezzo As Double, _
               pUnMis as String)
    With rsAppoggio
        .AddNew
        .Fields("TipoRiga").Value = pTipoRiga
  .Fields("DesRiga").Value = pDesRiga
        .Fields("QuantitaRiga").Value = pQuantita
  .Fields("PrezzoRiga").Value = pPrezzo
  .Fields("ImportoRiga").Value = pQuantita * pPrezzo
  .Fields("UnitaMisura").Value = pUnMis
    End With
End Sub

Naturalmente questo metodo non consente di gestire strutture a più livelli, essendo il recordset in questione "non-gerarchico".

Allo stesso modo possiamo invece creare strutture gerarchiche d’appoggio con le quali possiamo risolvere anche le problematiche più disparate.
Questa è sicuramente la soluzione più complessa e dispendiosa, ma permette veramente di risolvere tutte quelle situazioni apparentemente irrisolvibili.

Nella fattispecie si tratta di utilizzare la libreria ADOX per creare sul database una serie di tabelle che, riempite e legate tra loro in modo mirato, ci consentano di creare attraverso il comando SHAPE il recordset gerarchico da associare al Data Report.

Nel seguente esempio viene creata una tabella sul database:

Dim Catalogo as New ADOX.Catalog
Dim Table as New ADOX.Table

Set Catalogo.ActiveConnection = MyConnection
With Table
  .Name="tbAppoggio"
  .Columns.Append "TableKey", adInteger
  .Columns.Append "Campoxx", adChar, 10
End With

Catalogo.Tables.Append Table

Per organizzare correttamente la struttura d’appoggio bisogna considerare che un recordset gerarchico, creato attraverso l’impiego di un command primario e di vari command secondari, è così composto:

‘Struttura comando SHAPE
SHAPE {SELECT CampoA, CampoB, CampoC FROM TabellaA} AS cmTabellaA
  APPEND (( SHAPE {SELECT * FROM TabellaB} AS cmTabellaB
    APPEND ({SELECT * FROM TabellaC} AS cmTabellaC
    RELATE 'TabellaB.Key' TO 'TabellaC.Key') AS cmTabellaC)  AS cmTabellaB
    RELATE 'TabellaA.Key' TO 'TabellaB.Key') AS cmTabellaB

...risultato:

CampoA CampoB CampoC cmTabellaB

dove CampoA, CampoB e CampoC derivano dalla prima tabella specificata nel comando SHAPE, e cmTabellaB è l’oggetto Recordset che contiene i campi derivati dal command secondario legato alla TabellaA.
A sua volta il recordset ricavato dalla proprietà .Value del campo cmTabellaB, è composto esattamente come nello schema riportato (naturalmente i campi saranno diversi).

Set Rs = rscmTabellaA.Fields("cmTabellaB").Value

In questo modo possiamo quindi crearci una struttura particolare che contiene, in aggiunta ai campi da stampare, anche una serie di campi di totale, di decodifica a tabelle, ed altro ancora; il tutto facilmente stampabile sia nella parte relativa all’intestazione della sezione, che nel relativo piede.

Conclusioni
Come si può constatare, il Data Report non è poi così astruso da adoperare. Anche tenendo conto di alcuni suoi limiti, si tratta comunque di uno strumento abbastanza versatile per la maggior parte delle problematiche di stampa. Purtroppo certe soluzioni non sono propriamente intuitive, ed il tempo impiegato nell'apprendimento spesso non è direttamente proporzionale al risultato che si ottiene, e quindi per alcuni non rappresenterà certamente la soluzione migliore.
Nelle righe precedenti ho cercato (spero nel modo più semplice possibile) di illustrare le caratteristiche principali del Data Report Designer cercando, anche con l'ausilio di alcuni esempi, di indirizzare coloro che come me, desiderano restare indipendenti da applicativi di terze parti per la generazione delle stampe.
In un prossimo articolo cercherò di illustrare come bypassare la logica di stampa del Data Report, utilizzandolo in modo più avanzato.