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.