BENVENUTI  ::  CINEMA E TV  ::  INFORMATICA  ::  LETTERATURA  ::  MUSICA  ::  >  ::  PROFILO

Opinabile

è tutto ciò che scrivo, vedi tu se credermi o rendermi infelice...

mercoledì 4 novembre 2009

Crystal Report 2008: aggiunta a runtime di campi per l'ordinamento.

Abstract
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.
Soluzione
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

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
...

LR.

2 commenti:

  1. Grandissimo!! mi hai salvato! c'ero quasi, mi mancavano solo le righe per l'aggiunta tramite reflection dell'fd as FieldDefinition
    Grazie
    lTaurus

    RispondiElimina
  2. Grazie! Anche se con un bel po' di ritardo... grazie!
    (Accidenti, dovrei seguirlo di più questo mio blog!)

    RispondiElimina

Lascia un tuo commento...




ポストの決勝戦!


Lettori fissi...
se non funziona fai refresh (baco di Blogger!) altrimenti "segui" in alto a sx!

Seguo anche...

Piccolo spot per un amico...