Skip to content

API 2.0: Come possiamo fare per i report

La prima release delle API 2.0 di Xojo portano una gran quantità di novità ma, lasciano scoperti i report che saranno modificati in seguito. Vediamo come possiamo utilizzare le novità per i database pur utilizzando i report attuali.

Questo post, oltre a risolvere un problema iniziale di molti utenti, è utile per capire il meccanismo di estensione degli oggetti che semplifica di molto la programmazione in questo ambiente.

Vediamo le differenze tra API2.0 e API1.0 nell’accesso al database

Prendiamo come esempio quello che si trova negli esempi sotto Printing and Reporting, Reporting, Products e usiamo quello “List of products Preview”.

Il progetto non è stato aggiornato alle nuove API per cui troviamo questo codice nel pulsante per avviare il report:

// Il codice è lo stesso dell'originale con qualche piccola variante per renderlo più chiaro e corretto
// -------------------------
// Creiamo la variable per il nostro database
Dim ordersDB As New SQLiteDatabase
 
// Impostiamo il file del database a quello dato da una funzione che lo trova nel progetto
// per il nostro caso non ci interessa
OrdersDB.DatabaseFile = GetDBFile
 
// Se il file esiste non ci sono problemi
If OrdersDB.databaseFile.Exists Then
 
  //Il file esiste, proviamo a connetterci
  If Not OrdersDB.Connect Then
 
    // Ci sono stati problemi nella connessione per cui mostriamo
    // un messaggio e usciamo da questa routine
    MsgBox "Database Error: " + Str(ordersDB.ErrorCode) + EndOfLine + EndOfLine + OrdersDB.ErrorMessage
    Return
  End If
Else
  //Il file non esiste, mostriamo il messaggio e usciamo
  MsgBox("Database not found.")
  Return
End If
 
// Costruiamo il comando SQL per selezionare i record
Dim sql As String = "SELECT * FROM Products"
 
 
// Selezioniamo i record dal database
Dim rs As recordSet
rs = ordersDB.sqlSelect(sql)
 
// Controlliamo i dati
If rs = Nil Then
  // Qualcosa è andato storto non abbiamo ottenuto un risultato
  MsgBox "Database Error: " + Str(ordersDB.ErrorCode) + EndOfLine + EndOfLine + OrdersDB.ErrorMessage
Else if rs.EOF then
  // Non ci sono dati lo segnaliamo all'utente
  Beep
  MsgBox "No records found to print."
Else
  // Tutto Ok inviamo i nostri dati al container per la preview
  // Creiamo la variable per il nostro report
  Dim rpt As New ListOfProducts
  ReportPreviewContainer1.ShowReport(rpt, rs)
End If

Ora vediamo come possiamo scrivere lo stesso codice con le API2.0

// Per quanto l'uso di Var invece di Dim è opzionale, qui lo usiamo per mostrare che è codice API2.0
// -------------------------
// Creiamo la variable per il nostro database
Var ordersDB As New SQLiteDatabase
 
// Impostiamo il file del database come prima (niente di nuovo fino a qui)
OrdersDB.DatabaseFile = GetDBFile
 
// Se il file esiste non ci sono problemi
If OrdersDB.databaseFile.Exists Then
  //Il file esiste, proviamo a connetterci
  Try
    ordersDB.Connect
  Catch err As DatabaseException
    // Ci sono stati problemi nella connessione per cui mostriamo
    // un messaggio e usciamo da questa routine
    MessageBox "Database Error: " + err.ErrorNumber.ToString + EndOfLine + EndOfLine + err.Message
 
    Return
  End Try  
Else
  //Il file non esiste, mostriamo il messaggio e usciamo
  MessageBox("Database not found.")
  Return
End If
 
// Costruiamo il comando SQL per selezionare i record
Var sql As String = "SELECT * FROM Products"
 
// Selezioniamo i record dal database
Var rs As RowSet
Try
  rs = ordersDB.SelectSQL(sql)
Catch err As DatabaseException
  // Qualcosa è andato storto non abbiamo ottenuto un risultato
  MessageBox "Database Error: " + err.ErrorNumber.ToString + EndOfLine + EndOfLine + err.Message
  Return
End Try
If rs.AfterLastRow Then
  // Non ci sono dati lo segnaliamo all'utente
  System.Beep
  MessageBox "No records found to print."
Else
  // Tutto Ok inviamo i nostri dati al container per la preview
  // Creiamo la variable per il nostro report
  Dim rpt As New ListOfProducts
  ReportPreviewContainer1.ShowReport(rpt, rs)
End If

Fin qui tutto funziona come prima, però sfruttiamo le nuove API, quindi gestiamo le eccezioni invece di controllare o il codice di errore o qualche funzionalità errata. Il codice risulta più chiaro e leggibile.

Il problema ora è che nel nostro esempio il metodo showReport del Container ReportPreviewContainer, si aspetta come parametro un recordSet e non un RowSet.

Poco male, basta cambiare l’intestazione e ovviamente il tipo della variable mData. Nel metodo poi l’unica modifica da fare è modificare data.MoveFirst in data.MoveToFirstRow. Ma per il resto non abbiamo problemi.

Il problema

Il nostro codice però non funziona. Questo perché nel metodo showReport viene richiamato il metodo Run del report e questo si aspetta che il primo parametro sia un RecordSet.

Questa volta non possiamo operare a livello di codice direttamente. Però possiamo creare un modulo che ci risolve il problema in attesa di una soluzione “ufficiale”.

Creiamo quindi un modulo che chiamiamo reportExtension. Che alla fine avrà una struttura come mostrata nell’immagine.

Aggiungiamo una classe Privata che chiamiamo RowSetBridge

Aggiungiamo due proprietà private: rs as RowSet e types as Dictionary.

Selezioniamo la classe e nell’inspector premiamo il pulsante Choose per aggiungere l’interfaccia che ci serve. Dalla finestra che mettiamo il segno di spunta su Reports.Dataset, assicuriamoci che lo scopo sia pubblico e premiamo OK.

In pratica sfruttiamo una funzionalità dei report, che in realtà non vogliono un recordSet ma semplicemente una classe che implementi questa interfaccia.

Aggiungiamo un metodo Constructor che utilizziamo per instanziare correttamente la classe:

Public Sub Constructor(theRowSet as RowSet)
  // Impostiamo la proprietà rs alla RowSet che abbiamo come argomento
  rs=theRowSet
End Sub

Ora implementiamo i metodi dell’interfaccia:

Public Function EOF() as Boolean
  // Part of the Reports.Dataset interface.
  // EOF è equivalente a RowSet.IsAfterLastRow
  Return rs.AfterLastRow
End Function
 
Public Function Field(idx As Integer) as Variant
  // Part of the Reports.Dataset interface.
  // Nei dataSet i campi sono a base 1, RowSet è a base 0
  Return rs.columnAt(idx-1)  
End Function
 
Public Function Field(name As String) as Variant
  // Part of the Reports.Dataset interface.
  // Per qualche motivo c'è una chiamata con nome vuoto che
  // ovviamente crea problemi, evitiamolo
  If name<>"" Then
    // Restituiamo il campo del RowSet per nome
    Return rs.Column(name)
  End If
End Function
 
Public Function NextRecord() as Boolean
  // Part of the Reports.Dataset interface.
  // Se siamo alla fine dei dati restituiamo Falso
  If rs.AfterLastRow Then
    Return False
  Else
    // Altrimenti ci muoviamo al record successivo e restituiamo Vero
    rs.MoveToNextRow
    Return True
  End If  
End Function
 
Public Sub Run()
  // Part of the Reports.Dataset interface.
  // Stiamo eseguendo il report, creiamo il dictionary dei tipi e ci portiamo all'inizio dei dati
  types=New dictionary
  rs.MoveToFirstRow  
End Sub
 
Public Function Type(fieldName as string) as integer
  // Part of the Reports.Dataset interface.
  // Il metodo più complicato
  // visto che nel RowSet la proprietà columntype è interrogabile solo per indice
  // dobbiamo utilizzare un loop su ogni campo per trovarne l'indice
  // e poi ne otteniamo il columtype.
  // Utilizziamo un dictionary per evitare di fare il loop ogni volta
  // visto che viene richiesto per ogni record
 
  If Not types.HasKey(fieldName.Lowercase) Then
    //Se già non so il tipo, lo cerco
    For i As Integer=0 To rs.ColumnCount
      If rs.ColumnAt(i).Name=fieldName Then
        // Se il nome corrisponde assegno il valore ottenuto e esco dal loop
        types.Value(fieldName.Lowercase)=rs.ColumnType(i)
        Exit
      End If
    Next
  End If
 
  // Restituiamo il valore avendo cura di prevedere un caso
  // nel remoto caso in cui non troviamo un valore adatto
  // e in quel caso restituiamo 5 ovvero il tipo Stringa
  // opzionalmente potremmo generare una eccezione
  Return types.Lookup(fieldName.Lowercase, 5)  
End Function

Ora la classe ha tutto il necessario per funzionare. Manca solo un metodo per eseguire correttamente il report, ma è veramente semplice a questo punto:

Public Function Run(extends rpt as Report, data as RowSet, printerSetting as PrinterSetup) as Boolean
  Return rpt.Run(new RowSetBridge(data), printerSetting)
End Function

In pratica estendiamo la classe Report facendo l’overload di Run in modo da accettare il RowSet come primo parametro. Poi utilizziamo la nostra classe RowSetBridge per lanciare il Report correttamente.

In questo modo possiamo utilizzare tutte le funzionalità delle API 2.0 anche con i report, seppure non ufficialmente supportato.