Press "Enter" to skip to content

1 – Codedom Introduzione all’uso

Tutti abbiamo visto come una WinForm .NET quando lavoriamo con il Designer produca automaticamente del codice in un file chiamato .designer, questo codice viene generato da una serie di classi contenute nel Namespace System.Codedom, generare codice non è prerogativa di Visual studio, anche noi possiamo creare delle applicazioni che generano codice utilizzando gli stessi strumenti. In questo primo articolo introdurremo il “come si fa” e definiremo che cosa vogliamo produrre per poi vedere nelle successive puntate come creare ciò che serve a produrre codice nel linguaggio a noi più congeniale.

Introduzione

Quante volte quotidianamente ci troviamo ad avere la necessità di generare classi praticamente identiche? Il classico esempio sono le classi Entity e le classi che io chiamo Data Provider, quelle che forniscono i servizi base di aggiornamento dati su una tabella database. Ciò che cambia in queste classi sono nomi e numero di proprietà, oppure nomi tipi e numero di campi database ma sostanzialmente poi metodi, proprietà e quant’altro sono praticamente identici oppure riconducibili a qualcosa di parametrico.

E’ vero, esistono gli snippet di codice, che forniscono il modo di creare codice con dei placeholder, però uno snippet non è in grado di crearmi le variabili member e le property per una entity con 30 campi in una sola passata,devo usare trenta volte lo snippet di creazione variabile e property.

System.CodeDom, è il Namespace di .Net che contiene classi e servizi per la generazione di codice. E’ un namespace quasi sconosciuto ai più, credo che solo coloro che progettano componenti lo conoscano e lo usino intensamente, infatti, come già indicato nel sommario, è lui che produce tutte quelle righe di codice che vengono inserite dentro ai files .designer che stanno dietro alle form o ai componenti quando creiamo codice usando il designer visuale. E’ sempre lui anche ciò che sta dietro a tutti i wizard di Visual Studio che generano classi dati a partire da una tabella o una query su database.
Il suo scopo è molteplice perché non solo è in grado di generare codice e scriverlo su file, è anche in grado di generare codice “al volo” a runtime e compilarlo, ma per ora ci concentreremo sulla prima funzionalità.

Per chi volesse dare un’occhiata alla documentazione ufficiale su CodeDom, su MSDN parta da qui non troverà molto di più e cercando letteratura in merito, esiste ben poco, il testo su cui io ho costruito le mie conoscenze si intitola Code Generation in Microsoft.Net è piuttosto vecchio,  infatti risale al framework 1.1 e non ne ho trovato nuove edizioni più recenti, la scrittrice  consiglia l’uso di CodeDom nello specifico a chi necessita di scrivere codice sia in C# che in VB, preferendo usare la generazione “Brute Force” per chi usa un linguaggio unico. (se non sapete cos’è la generazione Brute Force, pensate ad uno string builder e tanti string format con output su un file .cs o .vb).

Una volta che avrete visto che cosa serve per generare una semplicissima classe con CodeDom, probabilmente darete ragione a lei, io invece, capito come funziona CodeDom, anche se sono necessarie giornate di lavoro per creare un generatore di codice funzionante, lo preferisco per molti motivi al generatore Brute force. Le giornate necessarie a costruire una classe che genera codice, saranno risparmiate decine di volte  in seguito ed il codice che genera è sintatticamente corretto, non solo ma è anche riproducibile in qualsiasi momento.

Per capire quanto tempo si risparmia, vi posso portare l’esempio di quello che ho fatto io: Per produrre l’applicazione che genera le mie classi Data Provider e le stored procedure per le tabelle di un database SQL Server, ho lavorato per circa un mese.

Il tempo di produzione a mano di 4 stored procedure + una classe data provider per una tabella media con diciamo 10 campi, è di circa 3 ore di lavoro. (test di base compreso). (ovviamente se una persona passa le sue giornate facendo solo questo lavoro magari potrebbe divenire efficiente e risparmiare un’ora 🙂 ma sono sempre tantissime ore).

Il tempo di aggiornamento di una classe di questo tipo quando la tabella subisce una modifica (aggiunta di un campo, modifica di una lunghezza ecc.) è di 30/40 minuti, se il campo deve essere usato come filtro per la selezione dei dati anche più.

Per coloro che amano l’uso del Wizard di Visual Studio per creare i data provider, la generazione di una tabella col wizard richiede circa 10 minuti, se poi la modificate dovete rigenerarla e sono altri 10 minuti. Se poi a quello che genera il wizard fate delle modifiche manuali, ad ogni modifica della tabella database dovrete ovviamente riapplicare a mano tutte le modifiche con la possibilità (per non dire la certezza) di commettere errori o dimenticarne qualcuna, oltre al fatto che fare e rifare le stesse cose solo perché ad un analista scappa la voglia di aggiungere 3 campi a settimana alla tabella più complessa del vostro db è noioso ed avvilente. (e’ stato questo il motivo primario per cui ho cercato un alternativa al wizard standard).

Il tempo di prima produzione della stessa classe, con le stored procedure da parte del Mio Generatore di codice è di 30 secondi. La sua rigenerazione dopo una modifica è di 30 secondi.

Se inseriamo anche la parte descrittiva, quindi le resources in lingua per descrizioni colonne, tooltip e descrizioni di campo da usare sulle maschere, per la prima stesura aggiungiamo 15 minuti per una tabella complessa. Per l’aggiornamento 1 minuto per ogni campo aggiunto o modificato.

Ottimizzando la stesura iniziale, le classi di un nuovo database si creeranno tutte insieme, in una mezza giornata di lavoro si può creare le classi dati complete per un Database con 50 tabelle.

Se dovessimo farlo a manina sarebbero 3 x 50 = 150 ore di lavoro… Il risparmio è assolutamente mostruoso.

Il CodeDom: puzzle tridimensionali e scatole cinesi

Essendo il CodeDom uno strumento fatto per fare cose complesse e destinato a produrre cose per i programmatori non posso definirlo semplice nella sua struttura e lo definirei anche piuttosto complicato da utilizzare.
Se vi piacciono i puzzle tridimensionali, probabilmente lo apprezzerete e imparerete ad usarlo, se invece siete il tipo che davanti ad un puzzle da ricostruire dopo cinque minuti butta tutto all’aria e va via, allora CodeDom non è fatto per voi.

Ho paragonato CodeDom ad un puzzle tridimensionale, perché è formato di oggetti contenuti dentro altri oggetti, contenuti dentro altri oggetti che a loro volta contengono più oggetti. In effetti le scatole cinesi e le Matrioske sono un’altro buon paragone. Per creare una classe, si parte da un oggetto CodeNamespace, anzi, in verità si parte da almeno 2 oggetti CodeNamespace, dentro ad uno di questi si inseriranno una serie di oggetti di tipo CodeNamespaceImport, dentro l’altro un oggetto CodeTypeDeclaration, e dentro alla CodeTypeDeclaration si inizierà ad inserire degli oggetti di tipo CodeMemberField, CodeMemberProperty e CodeMemberMethod, ed in ciascuno di questi tre oggetti si inseriranno altri oggetti.

Tutto questo non è altro che la rappresentazione OOP del codice di una classe .NET. che guarda caso se guardata graficamente può essere rappresentata con un albero che parte dal Namespace e arriva alle singole righe di codice.

Scopo del Progetto

Lo scopo di questo progetto è quello di presentare almeno la parte base degli oggetti del CodeDom con un esempio pratico, la creazione dinamica di una classe a partire da una serie di righe di specifica inserite su un file di testo.

In particolare, questo è il dato di Input:

Using=System
Using=System.Text
Using=System.Collections.Generic
Namespace=Dnw.Entity
ClassName=Person; Classe entity che rappresenta i dati base di una persona
Field=mName;System.String;Nome della persona
Field=mFamilyName;System.String;Cognome della persona
Field=mBirthDate;System.DateTime;Data di nascita
Property=Name;mName;System.String;Nome della persona
Property=FamilyName;mFamilyName;System.String;Cognome della persona
Property=BirthDate;mBirthdate;System.String;Data di nascita
Method=IsValid;Name;null;FamilyName;null;BirthDate;DateTime.MinValue
Method=Compare;Name;string;FamilyName;string;BirthDate;date
Method=ToString;{0} {1} | {2};Name;FamilyName; BirthDate
OutFile=C:\codegen\person.cs

E questo è ciò che vogliamo ottenere da questo input:

//-------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.3074
//
//     Changes to this file may cause incorrect 
//     behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//--------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;

namespace Dnw.Entity
{

    /// <summary>
    ///  Classe entity che rappresenta i dati 
    ///   base di una persona
    /// </summary>
    public partial class Person
    {

        /// <summary>
        /// Indica il nome della classe predisposta per 
        /// la gestione delle eccezioni
        /// </summary>
        private string mClassName = 
           System.Reflection.MethodBase.GetCurrentMethod()
             .ReflectedType.Name;

        /// <summary>
        ///  Classe entity che rappresenta i 
        ///     dati base di una persona
        /// </summary>
        public Person()
        {
            this.mName = string.empty;
            this.mFamilyName = string.empty;
            this.mBirthDate = DateTime.Now;
        }

        /// <summary>
        /// Nome della persona
        /// </summary>
        private string mName;

        /// <summary>
        /// Cognome della persona
        /// </summary>
        private string mFamilyName;

        /// <summary>
        /// Data di nascita
        /// </summary>
        private System.DateTime mBirthDate;

        /// <summary>
        /// Nome della persona
        /// </summary>
        public virtual string Name
        {
            get
            {
                return mName;
            }
            set
            {
                this.mName = value;
            }
        }

        /// <summary>
        /// Cognome della persona
        /// </summary>
        public virtual string FamilyName
        {
            get
            {
            return mFamilyName;
            }
            set
            {
            this.mFamilyName = value;
            }
        }

        /// <summary>
        /// Data di nascita
        /// </summary>
        public virtual string BirthDate
        {
            get
            {
            return mBirthdate;
            }
            set
            {
            this.mBirthdate = value;
            }
        }

        /// <summary>
        /// Metodo per verificare la validità della classe.
        /// </summary>
        private bool IsValid()
        {
            return (((Name != null) && 
              (FamilyName != null)) && (BirthDate != null));
        }

        /// <summary>
        /// Metodo per comparare due classi.
        /// </summary>
        private int Compare(object obj)
        {
            int ret = -1;
            if (obj is Person)
            {
                Person val = (Person)obj;
                ret = StringHelper.Compare(this.Name, val.Name);
                if ((ret == 0))
                {
                    ret = StringHelper.Compare(
                       this.FamilyName, val.FamilyName);
                    if ((ret == 0))
                    {
                        ret = this.BirthDate.CompareTo(val.date);
                    }
                }
            }
            return ret;
        }

        /// <summary>
        /// Metodo per visualizzare la classe come stringa.
        /// </summary>
        private string ToString()
        {
            string.Format("{0} {1} | {2}", 
               Name, FamilyName,  BirthDate);
        }
    }
}

 

'------------------------------------------------------
' <auto-generated>
'     This code was generated by a tool.
'     Runtime Version:2.0.50727.3074
'
'     Changes to this file may cause 
'     incorrect behavior and will be lost if
'     the code is regenerated.
' </auto-generated>
'------------------------------------------------------

Option Strict Off
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Text

Namespace Dnw.Entity

    '''<summary>
    ''' Classe entity che rappresenta i 
    ''' dati base di una persona
    '''</summary>
    Partial Public Class Person

        '''<summary>
        '''Indica il nome della classe predisposta 
        '''per la gestione delle eccezioni
        '''</summary>
        Private mClassName As String = _
           System.Reflection.MethodBase.GetCurrentMethod() _
              .ReflectedType.Name

        '''<summary>
        ''' Classe entity che rappresenta i 
        '''   dati base di una persona
        '''</summary>
        Public Sub New()
            MyBase.New
            Me.mName = string.empty
            Me.mFamilyName = string.empty
            Me.mBirthDate = DateTime.Now
        End Sub

        '''<summary>
        '''Nome della persona
        '''</summary>
        Private mName As String

        '''<summary>
        '''Cognome della persona
        '''</summary>
        Private mFamilyName As String

        '''<summary>
        '''Data di nascita
        '''</summary>
        Private mBirthDate As Date

        '''<summary>
        '''Nome della persona
        '''</summary>
        Public Overridable Property Name() As String
            Get
                Return mName
            End Get
            Set
                Me.mName = value
            End Set
        End Property

        '''<summary>
        '''Cognome della persona
        '''</summary>
        Public Overridable Property FamilyName() As String
            Get
                Return mFamilyName
            End Get
            Set
                Me.mFamilyName = value
            End Set
        End Property

        '''<summary>
        '''Data di nascita
        '''</summary>
        Public Overridable Property BirthDate() As String
            Get
                Return mBirthdate
            End Get
            Set
                Me.mBirthdate = value
            End Set
        End Property

        '''<summary>
        '''Metodo per verificare la validità della classe.
        '''</summary>
        Private Function IsValid() As Boolean
            Return (((Name <> null)  _
                        AndAlso (FamilyName <> null))  _
                        AndAlso (BirthDate <> null))
        End Function

        '''<summary>
        '''Metodo per comparare due classi.
        '''</summary>
        Private Function Compare(ByVal obj As Object) As Integer
            Dim ret As Integer = -1
            If TypeOf obj Is Person Then
                Dim val As Person = DirectCast(obj, Person)
                ret = StringHelper.Compare(Me.Name, val.Name)
                If (ret Is 0) Then
                    ret = StringHelper.Compare _
                      (Me.FamilyName, val.FamilyName)
                    If (ret Is 0) Then
                        ret = Me.BirthDate.CompareTo(val.date)
                    End If
                End If
            End If
            Return ret
        End Function

        '''<summary>
        '''Metodo per visualizzare la classe come stringa.
        '''</summary>
        Private Function ToString() As String
            string.Format("{0} {1} | {2}", 
               Name, FamilyName,  BirthDate)
        End Function
    End Class
End Namespace

Per ottenere tutto questo, scriveremo una ennesima classe Helper, scrivo ennesima perché sembra che i miei articoli siano costruiti solo per spiegare classi helper, come se nella programmazione .NET la maggior parte del codice fossero classi di questo tipo.

Non è così ovviamente però visto che le cose più interessanti per chi programma a mio avviso sono i mattoni da cui produrre applicazioni, piuttosto che applicazioni dimostrative inutili e inutilizzabili, dovrete sopportarmi così ancora per un po’.

Perché un’altra classe helper?

In effetti per creare un generatore di codice, visto che in System.Codedom ci sono tutti gli oggetti necessari si potrebbe anche procedere in modo diretto, però gli oggetti del CodeDom sono molto essenziali e con lo stesso oggetto si possono produrre molte cose, pertanto semplicemente per rendere potabile il loro uso (in modo che ogni volta che devo fare una modifica non mi serva una settimana per ricostruire mentalmente tutti gli oggetti CodeDom), ho preferito creare una classe con dei metodi statici che generano vari tipi di porzioni di codice e sono più semplici da ricordare e usare.

Il progetto per il case study

Aprite Visual Studio e create una nuova soluzione vuota, se usate una versione express che non contiene questo tipo di progetto, create una Libreria di Classi.

sc032_solution01

La soluzione si chiama DnwCodedom, al suo interno inseriamo 2 progetti, Un progetto libreria di classi che chiamiamo DnwCodedomCs ed una applicazione Windows che chiameremo DnwTestCodeDomCs.

Per prima cosa, apriamo le properties del progetto libreria e aggiorniamo il nome dell’assembly e il Default Namespace (attenzione a chi lo creerà in VB al Root Namespace che funziona in modo diverso da quello di C#.)

Ecco che cosa inserire:

DnwCodeDomCs

Assembly name: Dnw.CodeDom.Cs.v1.0

Default Namespace: Dnw.CodeDom

Build: Attvare la generazione della documentazione XML

Build Events: On successful build: call ..\..\publish.bat $(ConfigurationName) $(TargetName)

Il file Publish.bat va generato all’interno del progetto come file di content e contiene il seguente testo:

cd ..\..
call %windir%\system32\xcopy.exe bin\%1\%2.dll  "..\..\DllPublish\*.*" /s/e/v/y
call %windir%\system32\xcopy.exe bin\%1\%2.xml  "..\..\DllPublish\*.*" /s/e/v/y

call gacutil.exe /u %2 
call gacutil.exe /i bin\%1\%2.Dll

Vi rimando agli articoli della serie Fritto Misto, dove abbiamo iniziato a generare le librerie di base di DotNetWork per le spiegazioni relative alla GAC e alla creazione di Librerie generiche da utilizzare in tutti i nostri progetti pubblicandole in GAC e dotandole di firma.

Proseguiamo andando a firmare l’assembly sul tab “Signing” delle proprietà di progetto. Per firmarlo usiamo il KeyFile che abbiamo generato per Fritto Misto e salvato oppure creiamo un nuovo file di firma se non ne abbiamo ancora uno.

sc032_projsigning01

Vi ricordo che se non firmiamo la DLL il comando batch di Publish.bat darà errore.

All’interno del progetto Classe dati, creiamo una sola classe, CodeDomHelper, che sarà una classe static ovvero non instanziabile contenente solo metodi che ci aiuternanno a creare il generatore di codice.

DnwTestCodeDomCs

Questo progetto di tipo Windows forms application, conterrà una sola form al suo interno che ci aiuterà a creare la nostra classe dimostrativa e a testare i metodi della classe helper.

Anche in questo progetto modifichiamo il Namespace di default che modificheremo poi in Program.cs e in FrmMain.cs facendolo diventare Dnw.Test. Per il resto lasciamo tutte le opzioni di default, in quanto il progetto servirà solo a testare la libreria e a creare il generatore di prova. quando deciderete di farne uno vero potrete usare la dll pronta e creare un’altra libreria ad hoc per il progetto per cui volete generare il codice oppure prendere il progetto di test e trasformarlo in qualcosa di serio ;).

Le classi del CodeDom che useremo nel nostro progetto

Lavoreremo al progetto di test sulla prossima puntata di questa mini serie di articoli, in questa prima parte, vedremo invece quali sono alcuni degli oggetti di CodeDom che useremo per raggiungere il nostro scopo ovviamente in ordine sparso in base a come li ho usati nelle classi di test.

CodeExpression:

E’ una delle classi più usate nei parametri passati alle varie parti del generatore di codice, in realtà si tratta di una classe astratta (virtual) da cui derivano molte delle classi che generano degli “statements” all’interno di metodi, proprietà, dichiarazioni e assegnazioni.  ad esempio: CodeObjectCreateExpression, CodeCastExpression, CodeParameterDeclarationExpression, CodePrimitiveExpression,CodeTypeOfExpression, CodeSnippetExpression   sono classi che discendono da CodeExpression.

CodeNamespace:

La scatola che contiene tutto, la radice della nostra Tree o la mamma di tutte le matrioske. ovvero il namespace in cui la classe viene definita. Può anche non avere il nome (come si usa in VB :() ma è l’oggetto che poi viene passato al generatore .NET vero e proprio perché il codice sia fisicamente scritto.

CodeNamespaceImport:

La classe che permette di dichiarare le clausole using oppure imports o gli alias per gli oggetti in testa ad una classe.

CodeTypeDeclaration:

Questa è la classe che definisce un Tipo ovvero in .NET un oggetto, una classe, una Interfaccia, un’enumerazione o qualsiasi altro oggetto, per creare una classe ci servirà uno di questi oggetti, il ramo dell’albero più vicino alla radice rappresentata dal Namespace, o se preferite, la seconda matrioska.

CodeMemberField, CodeMemberMethod, CodeMemberProperty:

I tre tipi base di elemento che si inseriscono in una classe, la variabile member, il metodo, la property. queste in realtà non sono concentriche, ma sono tutte allo stesso livello.

CodeSnippetTypeMember:

Una delle classi il cui nome dice poche cose, perché non serve per un solo scopo, io l’ho usata per generare le Region all’interno di una classe. assieme alla seguente.

CodeRegionDirective:

Che permette di creare uno statement di apertura o chiusura di una region.

CodeVariableDeclarationStatement:

La classe che serve per dichiarare una variabile locale assegnandogli un valore.

CodeSnippetExpression:

La useremo in varie funzioni di generazione, ma quella per cui è più utile è generare del semplice codice contenuto in una stringa, quello che abbiamo definito “brute force”, di cui è difficile fare a meno anche usando il CodeDom per non diventare matti per la generazione di alcuni tipi di espressione per cui la complicazione della loro composizione non vale il codice scritto e una stringa può essere una soluzione, eventualmente con una if per scrivere la cosa giusta quando il linguaggio cambia da C# a VB.

CodeMethodReturnStatement:

E’ la classe che definisce una clausola return, sia per una property get che per un metodo.

CodeAssignStatement:

Definisce uno statement di assegnazione, quindi il classico Variabile = Espressione.

CodeArgumentReferenceExpression:

Permette di definire un argomento da passare ad una clausola Return piuttosto che ad un metodo.

CodeMethodInvokeExpression:

Permette di chiamare un metodo in uno statement.

CodeTypeReference:

Ci permette di dichiarare un riferimento ad un qualsiasi Type quindi ad una classe del framework oppure una classe definita da noi, per scrivere:

int miaVariabile; o Dim miaVariabile as Integer

Ci servirà una CodeTypeReference e una CodeObjectCreateExpression.

CodeObjectCreateExpression:

Una classe che permette di generare un oggetto a partire da una classe.

CodeParameterDeclarationExpression:

Simile alla precedente ma ci permette di generare un parametro nella dichiarazione di un metodo.

CodeCastExpression:

Ci permette di dichiarare un Cast di un oggetto assegnandolo poi ad una variabile o usandolo in qualsiasi altro tipo di espressione.

CodeTypeOfExpression:

Ci permette di generare un espressione typeof (o la corrispondente VB GetType(MyClass)).

CodeCommentStatementCollection:

Classe che contiene una collezione di righe di commento che verranno generate usando la classe seguente.

CodeCommentStatement:

Una classe che ci permette di generare commenti di tipo Documentale, (quelli in XML) oppure commenti normali. Quelli che noi programmatori usiamo sempre con esagerata parsimonia  :D:D:D:D.

CodeConditionStatement:

Una classe che ci permette di creare un espressione condizionale dentro a cui poi scrivere codice ovvero uno statement if ed il suo else

CodeBinaryOperatorExpression:

Una classe che ci permette di creare tutte le espressioni binarie, sia di tipo bitwise che logiche, ovvero tutte le espressioni che inseriamo usualmente all’interno delle clausole if, while, for, do while

Per generare effettivamente il codice dopo aver creato tutti gli oggetti che lo compongono, sono necessari alcuni oggetti:

CodeCompileUnit:

E’ la classe in cui vengono riversati gli oggetti generati per produrre il codice.

CSharpCodeProvider:

Classe di sistema che permette di generare il codice C#

VBCodeProvider:

Classe contenuta in Microsoft.Visualbasic che permette di generare il codice VB, non è standard perché VB non è un linguaggio standard ma è una creatura di Microsoft, mentre C# è standard del framework .NET.

CodeGeneratorOptions:

Classe che fornisce al provider tutte le opzioni per la stesura del codice, quali indentazioni, spaziature, ordine di scrittura del codice ecc.

Conclusioni

Essendo un articolo introduttivo mi fermo qui, sappiate che il numero di classi del CodeDom è molto più grande, infatti non abbiamo ancora introdotto gli statement try catch finally e le istruzioni di iterazione for, do, while ne abbiamo introdotto delegate, event handler, e simili. Nulla vieta che ne parliamo in seguito, se qualcuno deciderà di chiederlo.

Per qualsiasi domanda, curiosità approfondimento, o se trovate un errore usate pure i l link al modulo di contatto in cima alla pagina.