Questo mostra come sopperire ad una mancanza delle API di Crystal Reports 2008, ovvero come fare quando si vuole "comodamente" aggiungere dinamicamente dei campi di ordinamento in un report rpt.
Requisiti
Si assume che si abbiano minime basi di Crystal Reports e .NET.
Il codice riportato negli esempi è in VB.NET.
Il codice dei link di riferimento è in C#.
Esigenza
L'esigenza mi è nata quando ho dovuto fare una migrazione da un programma VB6, scritto per l'engine di Crystal Reports 8.5, ad una versione in VB.NET, per Crystal Reports 2008.
Problema
Sono profondamente cambiate molte cose nell'object model di Crystal Reports 2008 rispetto a quello di versione 8.5, ma non ne parlerò in questa sede: è un discorso troppo vasto; inoltre siti più mirati di questo possono essere più utili (tra gli italiani consiglio un giro su .net hell.it ). Qui mi basterà soltanto accennare al fatto che il modello di Crystal Reports 2008 oltre ad avere un comportamento diverso dal precedente 8.5 (i wrapper in .Net sui "vecchi" oggetti COM "ragionano" in modo differente dall'approcio 8.5) ha anche strane limitazioni: può darsi che sia la nuova filosofia del modello .NET nella reportistica; resta il fatto, comunque, che queste stranezze provochino impedimenti non banali nelle fasi di porting da un progetto per report Crystal pre-.NET ad uno per report Crystal e .NET.
Uno frai tanti casi può essere la non più supportata possibilità di aggiungere campi di ordinamento via programma (a runtime).
Con un minimo di ricerche e scopro che:
- Prima del supporto .NET il report manipolato dal programma poteva avere o meno i campi d'ordinamento. Con una banale istruzione delle API di Crystal Reports 8.5 ActiveX runtime si scrive (VB6):
report.RecordSortFields.Add(nome_campo, flag)
cioè: tramite l'istanza report (l'oggetto "wrapper" del mio rpt) recupero l'insieme dei campi di ordinamento (quelli per l'order by, per intendersi) e aggiungo ad essi la colonna che voglio (naturalmente deve esistere nelle tabelle dell'rpt, pena errore runtime), indicando il tipo di ordinamento (ASC o DESC). Per esempio:
report.RecordSortFields.Add("clienti.ragione_sociale", crAscendingOrder)
- Con la libreria per .NET e Crytal Reports 2008 (CrystalDecision.CrystalReports...) non posso fare la stessa cosa.
Dal Namespace CrystalDecisions.CrystalReports.Engine si ha la classe ReportDocumet, l'oggetto "wrapper" per l'rpt. L'object model prevede che ReportDocumet abbia in sè la definizione dati del report accessibile tramite l'oggetto DataDefinition. Un passo ancora ed è fatta: dalla definizione dei dati è possibile recuperare i campi di ordinamento tramite la collezione di SortField, SortFields appunto. Esempio di accesso alla collezione SortFields:
reportdoc.DataDefinition.SortFields
I SortField sono i campi di ordinamento che il progettista rpt ha introdotto nel report via designer: se il questi non ne ha messi, la collezione SortFields che ci ritroveremo a runtime sarà vuota. Quindi, un ciclo di for come il seguente, per esempio, non eseguirebbe nemmeno una istruzione all'interno del suo blocco:
For Each sf As SortField In reportdoc.DataDefinition.SortFields
' istruzioni
Next
Questo perché la collezione SortFields è in sola lettura.
Perché? Non lo so, mi mancano elementi per rispondere a questo quesito.
Su Internet ho trovato questa nota che forse può aiutare se non altro ad accettare tale realtà.
Come ovviare senza perdersi nei meandri di fix, articoli e varie? Con la Reflection.
La via più "breve" è quella di fare ciò che il vecchio metodo Add faceva: se il designer 2008 lo fa (di aggiungere i campi di order by) lo posso fare anch'io; ma serve la Reflection (quel meccanismo che permette di recuperare metodi, attributi etc. da un'istanza, a prescindere dalla visibilità, etc.).
Questo articolo (dal sito DECOMPILER-VB .net) mi è stato di fondamentale aiuto. E' scritto da uno che sa "smanettare" ben bene. Nella spiegazione (in inglese) viene descritto e riportato a passo a passo il codice C#. Manca un passaggio (solo descritto, ma senza relativo codice): l'aggiunta del nostro campo; ma in sostanza è completo.
Quindi ho riscritto del codice VB.NET per crearmi il metodo che mi serviva.
Nel mio progetto ho implementato in un modulo la seguente funzione:
Public Function AddSortFiled(ByRef reportdoc As ReportDocument, ByRef fd As FieldDefinition) As Boolean
Dim result As Boolean
Dim sfs As SortFields
Dim rassort As Object
Dim rassorts As Object
Dim rasfield As Object
Dim getrassorts As MethodInfo
Dim addsort As MethodInfo
Dim setsortfield As MethodInfo
Dim getrasfield As MethodInfo
Dim cirassort As ConstructorInfo
Dim rasassembly As Assembly
result = False
If Not reportdoc Is Nothing OrElse Not fd Is Nothing Then
sfs = reportdoc.DataDefinition.SortFields
getrassorts = sfs.GetType().GetMethod("get_RasSorts", BindingFlags.NonPublic Or BindingFlags.Instance)
rassorts = getrassorts.Invoke(sfs, System.Type.EmptyTypes)
addsort = rassorts.GetType().GetMethod("Add")
rasassembly = getrassorts.ReturnType.Assembly
cirassort = rasassembly.GetType("CrystalDecisions.ReportAppServer.DataDefModel.SortClass").GetConstructor(BindingFlags.Public Or BindingFlags.Instance, Nothing, System.Type.EmptyTypes, Nothing)
rassort = cirassort.Invoke(System.Type.EmptyTypes)
setsortfield = rassort.GetType().GetMethod("set_SortField", BindingFlags.Public Or BindingFlags.Instance)
getrasfield = fd.GetType().GetMethod("get_RasField", BindingFlags.NonPublic Or BindingFlags.Instance)
rasfield = getrasfield.Invoke(fd, System.Type.EmptyTypes)
setsortfield.Invoke(rassort, New Object() {rasfield})
addsort.Invoke(rassorts, New Object() {rassort})
result = True
End If
Return result
End Function
Dim result As Boolean
Dim sfs As SortFields
Dim rassort As Object
Dim rassorts As Object
Dim rasfield As Object
Dim getrassorts As MethodInfo
Dim addsort As MethodInfo
Dim setsortfield As MethodInfo
Dim getrasfield As MethodInfo
Dim cirassort As ConstructorInfo
Dim rasassembly As Assembly
result = False
If Not reportdoc Is Nothing OrElse Not fd Is Nothing Then
sfs = reportdoc.DataDefinition.SortFields
getrassorts = sfs.GetType().GetMethod("get_RasSorts", BindingFlags.NonPublic Or BindingFlags.Instance)
rassorts = getrassorts.Invoke(sfs, System.Type.EmptyTypes)
addsort = rassorts.GetType().GetMethod("Add")
rasassembly = getrassorts.ReturnType.Assembly
cirassort = rasassembly.GetType("CrystalDecisions.ReportAppServer.DataDefModel.SortClass").GetConstructor(BindingFlags.Public Or BindingFlags.Instance, Nothing, System.Type.EmptyTypes, Nothing)
rassort = cirassort.Invoke(System.Type.EmptyTypes)
setsortfield = rassort.GetType().GetMethod("set_SortField", BindingFlags.Public Or BindingFlags.Instance)
getrasfield = fd.GetType().GetMethod("get_RasField", BindingFlags.NonPublic Or BindingFlags.Instance)
rasfield = getrasfield.Invoke(fd, System.Type.EmptyTypes)
setsortfield.Invoke(rassort, New Object() {rasfield})
addsort.Invoke(rassorts, New Object() {rassort})
result = True
End If
Return result
End Function
In pratica nei parametri passo il campo del report (presente nel report!) che voglio indicare come campo di ordinamento e lo aggiungo alla lista dei campi di ordinamento (SortFields) "wrappandolo" in un un'istanza di SortField (sarà questa ad essere effettivamente aggiunta alla collezione).
Conclusione
Missione compiuta. Ora nel mio programma posso scrivere qualche cosa come:
...
Dim lastindex As Integer
Dim t As String
Dim c As String
Dim f As DatabaseFieldDefinition
Dim sf As SortField
...
' Campo da inserire nell'ordinamento
t = "tabella"
c = "campo"
' Recupero campo dalla tabella
f = reportdoc.Database.Tables.Item(t).Fields.Item(c)
' Tentativo di inserimento nella lista dei campi di ordinamento
If AddSortFiled(reportdoc, f) Then
' istruzioni per il setting del nuovo campo:
' ora lo posso fare perché è presente in SortFields
' (è l'ultimo inserito)
lastindex = reportdoc.DataDefinition.SortFields.Count - 1
sf = reportdoc.DataDefinition.SortFields.Item(lastindex)
' Per esmpio in ASC
sf.SortDirection = CrystalDecisions.Shared.SortDirection.AscendingOrder
sf.Field = f
Else
' istruzioni insuccesso
End If
...
For Each sf In reportdoc.DataDefinition.SortFields
' istruzioni
Next
...
Dim lastindex As Integer
Dim t As String
Dim c As String
Dim f As DatabaseFieldDefinition
Dim sf As SortField
...
' Campo da inserire nell'ordinamento
t = "tabella"
c = "campo"
' Recupero campo dalla tabella
f = reportdoc.Database.Tables.Item(t).Fields.Item(c)
' Tentativo di inserimento nella lista dei campi di ordinamento
If AddSortFiled(reportdoc, f) Then
' istruzioni per il setting del nuovo campo:
' ora lo posso fare perché è presente in SortFields
' (è l'ultimo inserito)
lastindex = reportdoc.DataDefinition.SortFields.Count - 1
sf = reportdoc.DataDefinition.SortFields.Item(lastindex)
' Per esmpio in ASC
sf.SortDirection = CrystalDecisions.Shared.SortDirection.AscendingOrder
sf.Field = f
Else
' istruzioni insuccesso
End If
...
For Each sf In reportdoc.DataDefinition.SortFields
' istruzioni
Next
...
LR.
Grandissimo!! mi hai salvato! c'ero quasi, mi mancavano solo le righe per l'aggiunta tramite reflection dell'fd as FieldDefinition
RispondiEliminaGrazie
lTaurus
Grazie! Anche se con un bel po' di ritardo... grazie!
RispondiElimina(Accidenti, dovrei seguirlo di più questo mio blog!)