Quando ci si trova davanti alla necessità di stampe di dati aziendali, grafici e quant'altro quasi sempre si ricorre a strumenti di reportistica. Questo perchè sia con grandi sia con piccole moli di dati il le attività per visualizzare, impaginare, costruire, e fare manutenzione di stampe e Report sono sempre complicate e richiedono notevoli quantità di tempo se non supportate da designer efficienti.
Crystal Report, come tanti altri tool di reportistica, è uno strumento avanzato per costruire pagine che visualizzano una serie di dati, calcoli, grafici e con possibilita' di esportazione in vari formati come
PDF,Excel e HTML che spesso sono le esigenze piu' richieste.
Con
Visual Studio .NET viene integrata una versione "Embedded" di
Crystal Report, il designer di report della
BusinessObjetcs, completo di quasi tutte le funzionalita' del pacchetto "stand-alone", con il vantaggio di essere velocemente integrabile nelle nostre applicazioni.
Lo sviluppo di un Report e l'integrazione nella nostra applicazione, consiste sostanzialmente in due operazioni distinte:
1) La creazione del file
".rpt" tramite l'editor nel quale vengono salvate tutte le informazioni necessarie al runtime per generare l'anteprima.
2) La scrittura di codice .NET per caricare il report, passare i dati, e tipicamente visualizzarlo tramite il
CrystalReportViewer (ma anche senza visualizzarlo potremmo usarlo direttamente per stamparlo, o esportarlo in qualche altro formato).
Aggiunto un Report ad un nostro progetto, ci dobbiamo preoccupare di fornirgli una sorgente dati con cui possiamo costruire il nostro risultato,operando sui campi a disposizione.
Tipicamente viene disegnato agganciando un database, ma è possibile anche dare Collections tipizzate basate su un nostri oggetti di Business, per questo articolo comunque, verra' usato un database
Access come sorgente dati.
Al momento dell'aggiunta del
Report, parte un
Wizard che ci aiuta nella costruzione, quindi la prima cosa è scegliere la sorgente dati, qui possiamo dare un'intera tabella, o possiamo definire un comando/statement customizzato, ovvero una stringa SQL, o possiamo costruirla tramite un
Query Builder:
Nel caso dovessimo cambiarla successivamente, la cosa è sempre possibile, basteà' entrare nella voce
"Esperto Database" (DataBase Expert in inglese) e modificare il set di dati come mostrato nell'animazione seguente (cliccare sull'immagine Thumbnail per vedere l'animazione):
In questo esempio la
Query che vogliamo è il classico "Master-Details" fatto utilizzando la
LEFT JOIN a livello di
Query. Perciò nel report avrà senso raggruppare (
GROUP BY)i dati secondo uno dei campi comuni, in modo da avere una struttura gerarchica senza ricorrere ai
Subreports che sono sicuramente più pesanti in fase di
Rendering del Report:
Ora dobbiamo passare alla parte di codice .NET per integrarlo dentro alla nostra applicazione:
Il report è un oggetto del tipo
ReportDocument, al quale bisogna dare la struttura del report,ovvero un File con estensione
.rpt.
Dopodichè lo passiamo come proprieta'
ReportSource ad un oggetto
ReportViewer (oppure usare direttamente per altri scopi, ma tipicamente un viewer si usa quasi sempre per dare un'anteprima interattiva all'utente).
ReportDocument report;
private void Form1_Load(object sender, EventArgs e)
{
report = new ReportDocument();
report.Load("ReportProducts.rpt");
CaricaDatiReport(report);
crystalReportViewer1.ReportSource = report;
crystalReportViewer1.RefreshReport();
}
private void filtraButton_Click(object sender, EventArgs e)
{
ReportDocument report = crystalReportViewer1.ReportSource as ReportDocument;
CaricaDatiReport(report);
report.RecordSelectionFormula = string.Empty;
if (logonRadioButton.Checked && !string.IsNullOrEmpty(cercaTextBox.Text))
report.RecordSelectionFormula = "{command.description} like '*" + cercaTextBox.Text + "*'";
crystalReportViewer1.RefreshReport();
}
A questo punto se mandiamo in esecuzione la nostra applicazione, il
Report funziona correttamente, la cosa che pero' dobbiamo considerare è che adesso il report punta ad un percorso di dati che in fase di Deploy quasi sicuramente cambiera'. La cosa da fare perciò, è passare i dati al nostro oggetto
Report, e qui abbiamo due metodi a disposizione:
1) Usare il metodo
SetDataSource2) Dare le impostazioni
LogonInfo al
Report, in modo che vada lui a recuperare i dati necessari
Con il primo metodo siamo noi da codice .NET a passare i dati al
Report, dando una sorgente dati che abbia la stessa struttura di quelli con cui abbiamo costruito il
Report. Qui di seguito un esempio:
private DataTable GetDatiFromDatabase(string ricerca,bool dataset)
{
DataTable table = new DataTable();
if (dataset)
{
//--- versione DataSet
DatiDataSetTableAdapters.MiaQueryTableTableAdapter queryDataAdapter = new DatiDataSetTableAdapters.MiaQueryTableTableAdapter();
if (!string.IsNullOrEmpty(ricerca))
table = queryDataAdapter.GetDataByDescrizione("%" + ricerca + "%");
else
table = queryDataAdapter.GetData();
}
else
{
//--- versione ADO.NET
string stringaSelect =
" SELECT products.*,categories.Description, categories.Manager FROM products " +
" LEFT JOIN categories ON products.CategoryID = categories.ID";
if (!string.IsNullOrEmpty(ricerca))
stringaSelect = stringaSelect + " WHERE description LIKE '%" + ricerca + "%'";
using (OleDbConnection conn = new OleDbConnection(ConfigurationManager.ConnectionStrings["ReportVisualStudio.Crystal.Properties.Settings.dbConnectionString"].ConnectionString))
{
OleDbCommand command = conn.CreateCommand();
command.CommandText = stringaSelect;
conn.Open();
table.Load(command.ExecuteReader());
}
}
return table;
}
In questo esempio tra l'altro, si usano due strade anche con lo stesso metodo
SetDataSource, ovvero caricare i dati in una
DataTable, o usare i metodi di un
DataSet tipizzato incluso nella nostra applicazione. Le prestazioni sono le stesse, dipende ovviamente da come è stata architettata l'applicazione in precedenza.
private void CaricaDatiReport(ReportDocument report)
{
if (logonRadioButton.Checked)
{
//--- passaggio dati di connessione al db
TableLogOnInfo info = new TableLogOnInfo();
info.ConnectionInfo.ServerName = "db.mdb"; //--- di default lo cerca sulla current
report.Database.Tables[0].ApplyLogOnInfo(info);
}
else
{
//--- caricamento dati tramite codice
report.SetDataSource(GetDatiFromDatabase(cercaTextBox.Text, datasetRadioButton.Checked));
}
}
Con la seconda invece sarà il
Report ad essere autosufficiente nel prendersi i dati dalla connessione a cui stiamo puntando, con il vantaggio di non dover fare niente al codice se i dati cambiano, basterà fornire all'utente un nuovo file
.rpt.
Dopo che abbiamo finito la fase di creazione del
Report, lo possiamo dare in pasto al
CrystalReportViewer il quale gestirà l'interattività con l'utente. Nell'esempio tra l'altro usando i raggruppamenti, il
Viewer ci abilita una feature abbastanza interessante, il
"DrillDown", ovvero la possibilità di "isolare" le pagine relative ai dati di un singolo valore di raggruppamento. Questo si attiva quando si passa con il mouse sopra al campo di raggruppamento e il puntatore diventa una lente: (cliccare sull'immagine Thumbnail per vedere l'animazione):
L'ultima cosa presente nell'esempio è una banale casella di ricerca, che permette di filtrare i dati da passare. Nel caso di utilizzo del metodo
SetDataSource è abbastanza facile da fare con la sintassi
SQL classica, o nel caso di un
DataSet Tipizzato, usando un metodo personalizzato. Nel caso invece dell'utilizzo di
LogonInfo, la cosa va fatta tramite l'impostazione della
Selection Formula, con una sintassi propria di
Crystal Reports.
Applicazione a runtime
Qui di seguito è possibile vedere un'animazione della nostra applicazione in esecuzione (cliccare sull'immagine Thumbnail per vedere l'animazione):
Conclusioni
Il metodo che personalmente preferisco è passare i dati con
SetDataSource, in quanto spesso i dati da visualizzare non sono tantissimi, ma soprattutto si hanno magari in memoria durante il funzionamento dell'applicazione stessa. Per questo passarli al report diventa il metodo più comodo e più "controllabile", soprattutto nel caso si debbano fare trasformazioni particolari sui dati, dove il codice -.NET ci aiuta molto, senza impazzire con "campi formula" dove la sintassi è più complessa e senza
IntelliSense.
La seconda invece ha senso per grandi moli di dati, tipicamente su stampe di statistiche per archivi grossi, e magari soggetti spesso a continue modifiche richieste da clienti, a discapito del controllo sui dati visualizzati.
In entrambi i casi un po' di codice va comunque scritto ma utilizzando il secondo metodo è sicuramente di meno. Per quanto riguarda le prestazioni sono praticamente uguali.
Accorgimenti usati nel codice di esempio
Se raggruppando i dati, desiderate che i valori del campo per cui si raggruppa non appaiano nella
Treeview di navigazione del
ReportViewer (tipicamente quando avete un numerico e preferite vedere la descrizione di un altro campo), potete cambiare il nome e lasciare il raggruppamento ugualmente su quel campo: (cliccare sull'immagine Thumbnail per vedere l'animazione)
Quando aggiungete dei file statici come un
Report .rpt, un
Database Access ecc. ricordatevi di specificare nelle proprietà del file,
BuildAction=none e
CopyToOutput=Always in modo che quando lanciamo l'applicazione vengano copiati nella
Bin.
Se fate uso di
DataSet Tipizzati e volete costruire la vostra
Query sui dati come nell'esempio, aggiungete un
DataAdapter basato sul vostro
statement SQL. In particolare nel caso del filtro serviva che ricevesse in ingresso un parametro:
Quando i dati dal
Database cambiano, o quando modificate la stringa di
SELECT, lanciate un
"Verify DataBase" per allineare il
Report: