Press "Enter" to skip to content

2 – Codedom Introduzione all’uso

In questa seconda parte della serie dedicata a Codedom, ci occuperemo di sviluppare il nostro progetto per creare una classe utilizzando CodeDom a partire da una serie di indicazioni scritte come un semplice testo. Creiamo la semplice interfaccia utente, e cominciamo ad elaborare il necessario affinché il bottone Genera produca quello che abbiamo immaginato.

La Form di Test

L’interfaccia utente fornita dal nostro progetto, sarà molto semplice, composta da una singola form che con grande
originalità abbiamo chamato FrmMain. Vediamo come costruirla:

sc_033_codedom2formstructure01

Ecco la struttura della form, in cui abbiamo inserito i seguenti componenti:

pnlTop: Panel, property modificate:

  • BackColor = GreenYellow;
  • Dock = top;
  • Size = xxx;48 (la dimensione x ovviamente è data dalla dimensione della form.
  • Padding = 4;4;4;4;

splTop: Splitter, property modificate:

  • Dock = top;
  • Size = xxx;3;

pnlBottom: Panel, property modificate:

  • BackColor = Turquoise;
  • Dock = bottom;
  • Size =xxx;220;
  • Padding = 4,4;4;4;

splBottom: Splitter, property modificate:

  • Dock = bottom;
  • Size = xxx;3;

pnlMid: Panel, property modificate:

  • BackColor = Coral;
  • Dock = Fill;
  • Padding = 4;4;4;4;

grbLanguage: GroupBox, inserita in pnlTop, propertyModificate:

  • BackColor = GreenYellow;
  • Size = 166;38;
  • Text = Language;

optCs: RadioButton, inserito in grbLanguage Property modificate:

  • Autosize = true;
  • BackColor = greenYellow;
  • Checked = true;
  • TabIndex = 0;
  • TabStop = true;
  • Text = C#;

optVB: RadioButton, inserito in grbLanguage Property modificate:

  • Autosize = true;
  • BackColor = greenYellow;
  • Checked = false;
  • TabIndex = 0;
  • TabStop = true;
  • Text = VB;

btnGenerate: Button, inserito in pnlTop, property modificate:

  • Size = 200;23;
  • Tabindex = 0;
  • tabstop = true;
  • Text = Genera;

txtTodo: Textbox, inserito in pnlMid, property modificate:

  • Dock = fill;
  • Multiline = true;
  • Scrollbars = vertical;
  • Text = string.empty;
  • Tabindex = 1;
  • tabstop = true;

txtResult: TextBox, inserito in pnlBottom, property modificate:

  • Dock = fill;
  • Multiline = true;
  • Scrollbars = vertical;
  • Text = string.empty;
  • Tabindex = 1;
  • tabstop = true;

sc_033_codedom2formstructure02

Il risultato di quanto impostato dovrebbe assomigliare all’immagine qui sopra.

A questo punto, iniziamo a scrivere codice, per prima cosa, visto che siamo pigri, definiamo una costante a livello della nostra classe FrmMain dove andremo a scrivere le note per la generazione della classe.

Se mai doveste creare un vero generatore di codice, è molto semplice implementare una textbox ed un bottone per caricare le note di generazione da un file di testo ed un bottone per salvare su file di testo le note da noi scritte.

Oppure, potete sbizzarrirvi a creare una interfaccia utente per la creazione delle classi ed una struttura XML per memorizzare i dati strutturali.

        private const string TXT_TestClass = @"
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
";

Al momento, io mi limito a questo.

        private void FrmMain_Load(object sender, EventArgs e)
        {
            this.txtTodo.Text = TXT_TestClass;
        }

E richiamo nell’event handler del form load il caricamento delle note nella textbox Todo.

Come potete notare, la forma delle note di creazione file è simile ad un file .INI, mi sembrava l’approccio più semplice, senza scomodare XML, almeno per iniziare. Ho creato una serie di Tag, che stanno sulla Sinistra di un uguale che forniscono dei valori, che sono inseriti sulla destra dell’uguale e possono essere multipli, in questo caso sono separati da punti e virgola. Così facendo, per capire che classe costruire, devo effettuare solo una serie di Split.

I Tag ed i loro valori

  • Using: Rappresenta una clausola using (C#) o imports (VB) il valore che lo segue è uno solo ed è un namespace.
  • Namespace: Rappresenta il Namespace in cui verrà inserita la classe, apparirà una sola volta ed avrà un solo valore, se ve ne fosse più di uno, solo il primo avrebbe uso.
  • Classname: rappresenta il nome della classe che stiamo generando, apparirà una sola volta ed avrà due valori, il nome della classe e la sua descrizione.
  • Field: Rappresenta una variabile member privata a livello di classe, deve essere seguita da tre valori separati da punti e virgola che rappresentano:
    • Nome della variabile member.
    • Tipo della variabile con il namespace completo, questo è indispensabile per darne la definizione usando il Codedom.
    • Descrizione della variabile, necessaria per creare un commento opportuno.
  • Property: Rappresenta una property pubblica con Get e Set, deve essere seguita da quattro valori separati da punti e virgola che rappresentano:
    • Nome della Property
    • Nome della variabile member corrispondente.
    • Tipo della property con il namespace completo.
    • Descrizione della property.
  • Method: Rappresenta un metodo, in questo caso, i parametri a destra dell’uguale non sono costanti, infatti io ho implementato 3 metodi nel mio generatore di codice, ciascuno si aspetta alcuni parametri,
    • IsValid, si aspetta un numero variabile di parametri composto di Nome;Valore per cui non è valido, ecco perché nella mia stringa è scritto come:
      IsValid;Name;null;FamilyName;null;BirthDate;DateTime.MinValue
      Il metodo isvalid controlla che Name sia diverso da null, FamilyName diverso da Null e BirthDate maggiore del valore minimo della data.
    • Compare, si aspetta una serie di parametri variabili composti da Nome;Tipo (convenzionale) perché scrive una serie di statement per verificare se due classi sono uguali, comparando varie property delle stesse.
      Compare;Name;string;FamilyName;string;BirthDate;date
      In questo caso abbiamo Name e FamilyName che sono stringhe e BirthDate che è una data.
    • ToString, si aspetta una stringa con placeholders per la formattazione delle righe ed una lista di parametri che corrispondono ai placeholders.
      ToString;{0} {1} | {2};Name;FamilyName; BirthDate
  • OutFile: Si aspetta un parametro che è il path del file in cui generare la classe.

Ovviamente si tratta di un modello piuttosto rozzo, che però potete ovviamente modificare ed immaginare in modo molto più strutturato se deciderete di creare un vero generatore di codice. Infatti le property potrebbero avere un parametro che stabilisce se sono read only o write only, e potrebbero generare automaticamente la loro variabile privata, oppure se si gradisce usare le nuove funzionalità del C# nel framework 3.5 e successivi, potreste creare una property con variabile member autogenerata. Per i metodi potrebbe essere anche corretto creare un Tag per ogni tipo di metodo (il Compare, il ToString e l’Isvalid in effetti sono una tipologia di metodo e potremo implementarne molti altri in una classe di questo tipo (ad esempio i metodi di serializzazione xml).

Spero comunque che quanto seguirà vi aiuti a farvi un idea di come partire con lo sviluppo, quel che posso consigliarvi è di evitare di pensare di creare un generatore generico, ma piuttosto creare vari generatori specializzati, uno per le Entity, uno per le collection, uno per le classi data provider, e così via, altrimenti perdete tutti i vantaggi e vi complicate la vita invece di semplificarla.

Le variabili a livello di classe

Oltre all’immancabile mClassName che trovate in tutte le mie classi ai fini della gestione degli errori, in questa classe abbiamo definito le seguenti variabili private:

        private CodeTypeDeclaration mClass;

        private CodeNamespace mCodeNamespace;

        private CodeConstructor mConstructor;

        private TargetLanguage mLanguage;

        private string mOutFile;

        private CodeNamespace mUsingNamespace;

mClass, è l’oggetto che contiene tutto il codice della classe (e a sua volta sarà contenuto da mCodeNamespace; mConstructor è il metodo costruttore della classe, che utilizzeremo per le inizializzazioni delle variabili private e quindi esporremo una volta creato a questo scopo. mLanguage definisce in che linguaggio scrivere la classe. mUsingNamespace è il namespace ove saranno inserite le clausole using (imports). mOutFile è il nome del file dove scriveremo la classe.

Il codice dietro al button click

        private void btnGenerate_Click(object sender, EventArgs e)
        {
            try
            {
                if (optCs.Checked)
                {
                    mLanguage = TargetLanguage.Cs;
                }
                else
                {
                    mLanguage = TargetLanguage.Vb;
                }
                string[] rows = this.txtTodo.Text.Split(Environment.NewLine.ToCharArray(),
                  StringSplitOptions.RemoveEmptyEntries);
                ProcessRows(rows);
                GenerateClassCodeAndFile();
                this.txtResult.Text = File.ReadAllText(mOutFile);
            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                    System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                Warnings.Errore(mClassName,
                    System.Reflection.MethodBase.GetCurrentMethod(), ex);
            }

        }

Il Bottone di generazione classe come possiamo vedere fa le seguenti cose:

  • Determina in che linguaggio scrivere la classe.
  • Legge il testo della definizione della classe e lo separa in righe.
  • Esegue una funzione che fa il parse delle righe di definizione e genera le classi per la costruzione del codice.
  • Genera fisicamente la classe sul file di output.
  • Legge il file e lo visualizza sulla textbox txtResult.

Il metodo ProcessRows

        private void ProcessRows(string[] rows)
        {
            try
            {
                List<KeyValuePair<string, string>> commands =                     new List<KeyValuePair<string, string>>();
                for (int i = 0; i < rows.Length; i++)
                {
                    string[] row = rows[i].Split(new char[] {'='}, 
                        StringSplitOptions.RemoveEmptyEntries);
                    if (row.Length >= 2)
                    {
                        commands.Add( new KeyValuePair<string,string>(row[0], row[1]));
                    }
                }

                ProcessCommands(commands);

            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                  System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + 
                     mClassName + GlobalConstants.TXT_DOT
                    + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Il metodo ProcessRows, ha il compito di individuare quali sono i tags predisposti nella serie dei comandi e creare una collezione di tipo Name -> Value che contiene la lista di quanto indicato. Fatto questo chiama il metodo ProcessCommands, che si occuperà invece di generare le classi codedom per la scrittura del codice.

Il metodo ProcessCommands

        private void ProcessCommands(List<KeyValuePair<string, string>> pCommands)
        {
            try
            {
                mUsingNamespace = CodeDomHelper.GetNamespace();
                mCodeNamespace = null;
                string[] components;
                foreach (KeyValuePair<string, string> kvp in pCommands)
                  {
                    switch (kvp.Key.ToLower())
                    {
                        case "namespace":
                            mCodeNamespace = new CodeNamespace(kvp.Value);
                            break;
                        case "using":
                            mUsingNamespace.Imports.Add(CodeDomHelper.GetUsing(kvp.Value));
                            break;
                        case "classname":
                            components = kvp.Value.Split(new char[] { ';' }, 
			StringSplitOptions.RemoveEmptyEntries);

                            GenerateBaseClass(components);
                            break;
                        case "field":
                            if (mClass != null)
                            {
                                components = kvp.Value.Split(new char[] { ';' }, 
				StringSplitOptions.RemoveEmptyEntries);
                                GenerateField(components);
                            }
                            break;
                        case "property":
                            if (mClass != null)
                            {
                                components = kvp.Value.Split(new char[] { ';' }, 
				StringSplitOptions.RemoveEmptyEntries);
                                GenerateProperty(false, components);
                            }
                            break;
                        case "propertyr":
                            if (mClass != null)
                            {
                                components = kvp.Value.Split(new char[] { ';' }, 
				StringSplitOptions.RemoveEmptyEntries);
                                GenerateProperty(true, components);
                            }
                            break;
                        case "method":
                            if (mClass != null)
                            {
                                components = kvp.Value.Split(new char[] { ';' }, 
				StringSplitOptions.RemoveEmptyEntries);
                                GenerateMethod(components);
                            }
                            break;
                        case "outfile":
                            this.mOutFile = kvp.Value;
                            break;

                    }
                }
            }
            catch (NoNamespaceException nEx)
            {
                throw nEx;
            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                    System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + 
		mClassName + GlobalConstants.TXT_DOT
                  + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Questo metodo è quello che fa il grosso del lavoro nel nostro generatore di codice, ovvero in base ai tags inseriti esegue la generazione del codice, ricordandovi che il progetto è solo un progetto di test, indico alcuni limiti di cui tenere conto se deciderete di creare il vostro generatore di codice: Le righe con i tags, sono in parte posizionali, se inseriste nel testo di definizione il namespace dopo il ClassName otterreste un errore, se inseriste il ClassName dopo la definizione delle variabili e dei metodi otterreste un errore. Se inseriste più volte Namespace e ClassName Otterreste un errore. Per migliorare le cose sarebbe opportuno inserire dei controlli nel metodo ProcessRows per posizionare correttamente gli elementi e scartare quelli doppi.

Come potete osservare, il metodo qui sopra, per prima cosa crea un namespace senza nome chiamato mUsingNamespace, questo serve per poter inserire le clausole using (imports) in testa alla classe. Il secondo namespace invece viene generato solo quando viene incontrato il tag che ne definisce il nome. Questo sarà il namespace che contiene la classe che andremo a generare. Per chi usa VB, potrebbe essere vuoto, se ponete tutte le classi nel Rootnamespace di progetto, questo tipo di approccio può andar bene sulle applicazioni, non molto invece se state sviluppando Librerie generiche, in questo caso sarebbe meglio definire una gerarchia di namespace che partono dalla root, usando delle cartelle all’interno del progetto.

Una volta definito il namespace, il metodo, effettua un ciclo sulla collection dei tags e in base al nome del Tag, lancia la funzione che crea una delle porzioni della nostra classe, vediamo quindi ciascuno dei metodi e come il Codedom costruisce la struttura della classe.

Il metodo GenerateBaseClass

        private void GenerateBaseClass(string[] components)
        {
            try
            {

                mClass = CodeDomHelper.GetClass(components[0]);
                this.mClass.IsClass = true;
                this.mClass.IsPartial = true;
                this.mClass.Comments.AddRange(
                    CodeDomHelper.GetSummaryComments(
                    components[1]));
                this.mClass.Attributes = MemberAttributes.Public;

//this.mClass.BaseTypes.Add(new CodeTypeReference("NomeClassePadre"));
//this.mClass.BaseTypes.Add(new CodeTypeReference("InterfacciaImplementata")); 
                if (mCodeNamespace != null)
                {
                    mCodeNamespace.Types.Add(mClass);
                    mClass.Members.Add(CodeDomHelper.GetMClassName());
                    mConstructor = CodeDomHelper.GetConstructorBase(
			MemberAttributes.Public, components[1]);

                    mClass.Members.Add(mConstructor);
                }
                else
                {
                    throw new NoNamespaceException("Non è stato definito il namespace!");
                }

            }
            catch (NoNamespaceException nEx)
            {
                throw nEx;
            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                    System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + 
 		mClassName + GlobalConstants.TXT_DOT
                    + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }

        }

Questo metodo, utilizza alcuni dei metodi della classe Helper CodeDomHelper che ho creato per fornire dei metodi “potabili” per semplificare a me stessa (e a chi legge) la vita quando generiamo una classe. La descrizione dei suoi metodi in dettaglio che mostra come costruire gli oggetti più interni al Codedom, l’ho lasciata per la terza puntata di questa serie di articoli, credo che così sia più semplice e logico per chi legge capire come usare questi oggetti, piuttosto che darvi prima le spiegazioni su come costruire i dettagli e poi invece usarli davvero.
GetClass, ci restituisce l’oggetto CodeTypeDeclaration (classe) e gli indichiamo il suo nome (components[0]). Gli diciamo che è una classe, che è una partial class, e gli aggiungiamo il commento XML in testa alla classe usando un’altro metodo dell’helper, GetSummaryComments a cui passiamo il secondo parametro (Components[1]);

Aggiungiamo la classe al Namespace che la contiene (mCodeNamespace) se il namespace non è stato definito diamo una exception tipizzata che verrà rispedita alla form per visualizzare un messaggio all’utente. A questo punto iniziamo ad aggiungere codice alla classe, per prima cosa aggiungiamo la variabile mClassName, e poi creiamo il Costruttore della classe che inseriremo nella classe ma lascieremo visibile ai metodi che costruiranno gli altri oggetti della classe in una variabile a livello di classe mConstructor, in questo modo potremo inserire nel costruttore l’inizializzazione di variabili ed altro codice necessario in base alle nostre esigenze. Anche per creare il costruttore, usiamo uno dei metodi helper.

Ho lasciato commentate due righe che ci permettono, volendo, di implementare la derivazione da altra classe oppure l’implementazione di una interfaccia.

Il metodo GenerateField

        private void GenerateField(string[] components)
        {
            try
            {
                Type tipo = Type.GetType(components[1]);

                CodeExpression val = CodeDomHelper.GetPlainCode("null");
                if (components[1].ToLower().Contains("string"))
                {
                    val = CodeDomHelper.GetPlainCode("string.empty");
                }
                if (components[1].ToLower().Contains("datetime"))
                {
                    val = CodeDomHelper.GetPlainCode("DateTime.Now");
                }

                this.mClass.Members.Add(CodeDomHelper.GetFieldVariable(components[0],
                    components[1], MemberAttributes.Private,
                    components[2], null));

                this.mConstructor.Statements.Add(CodeDomHelper.GetFieldVariableAssignment(
                    components[0], val));

            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                    System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + mClassName 
                    + GlobalConstants.TXT_DOT
                    + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Questo metodo si occupa di generare le variabili private della classe definite dai parametri Field. Per prima cosa, genera un Tipo utilizzando il nome completo di Namespace passatogli, in base a quello che noi gestiamo crea l’espressione di inizializzazione base. Poi utilizza il metodo GetFieldVariable dell’helper per generare la variabile nella classe e poi aggiunge al costruttore l’inizializzazione della variabile usando il metodo GetFieldVariableAssignment.

Il metodo GetProperty

        private void GenerateProperty(bool pReadonly, string[] components)
        {
            try
            {
                Type tipo = Type.GetType(components[2]);

                if (!pReadonly)
                {
                    this.mClass.Members.Add(CodeDomHelper.GetProperty(
                        components[0], components[1], tipo, components[3], true, true));
                }
                else
                {
                    this.mClass.Members.Add(CodeDomHelper.GetProperty(
                        components[0], components[1], tipo, components[3], true, false));
                }
            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                  System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + mClassName + 
                    GlobalConstants.TXT_DOT
                    + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Questo metodo si occupa di generare le property della nostra classe, anche in questo caso, vengono usati due overload di uno dei metodi del nostro helper, GetProperty, a cui passando nome, nome della variabile associata, tipo e commenti, oltre ai segnalini per decidere se readonly o meno, otteniamo la property da aggiungere alla classe.

Il metodo GenerateMethods

        private void GenerateMethod(string[] components)
        {
            try
            {
                switch (components[0].ToLower())
                {
                    case "isvalid":
                        GenerateIsValid(components);
                        break;
                    case "compare":
                        GenerateCompare(components);
                        break;
                    case "tostring":
                        GenerateToString(components);
                        break;
                }
            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                   System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + mClassName + 
                    GlobalConstants.TXT_DOT
                    + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Questo è un metodo di transito, che chiama il metodo di generazione corretto in base ai parametri indicati.

Il metodo GenerateToString

        private void GenerateToString(string[] components)
        {
            try
            {
                CodeMemberMethod mtd = CodeDomHelper.GetMethod(components[0],
                  "System.String", "Metodo per visualizzare la classe come stringa.");
                mClass.Members.Add(mtd);

                List<CodeExpression> para = new List<CodeExpression>();
                para.Add(CodeDomHelper.GetString(components[1]));
                for (int i = 2; i < components.Length; i++)
                {
                    para.Add(CodeDomHelper.GetPlainCode(components[i]));
                }

                CodeExpression code = CodeDomHelper.GetMethodCall(
                    "string", "Format", para.ToArray());
                CodeMethodReturnStatement ret = new CodeMethodReturnStatement();
                ret.Expression = code;
                mtd.Statements.Add(ret);
            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                  System.Reflection.MethodBase.GetCurrentMethod(), 
		ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + 
		mClassName + GlobalConstants.TXT_DOT
                  + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Questo metodo genera il ToString per la nostra classe, per farlo, per prima cosa genera il metodo usando il metodo helper GetMethod e lo aggiunge alla collezione Members della classe.  Poi genera le espressioni  necessarie, prima di tutto la stringa di formattazione, poi ognuno dei parametri utilizzando GetPlainCode, ed infine genera la chiamata al metodo String.Format usando GetMethodCall. Fatto questo, genera il return che restituisce al chiamante la stringa che rappresenta la classe usando una generazione diretta di CodeMethodReturnStatement. Il codice generato viene poi aggiunto alla collezione Statements del metodo precedentemente generato ed aggiunto alla classe. Questo Metodo genererà il seguente codice per la classe esempio:

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

Il Metodo Generate IsValid

        private void GenerateIsValid(string[] components)
        {
            try
            {
                CodeMemberMethod mtd = CodeDomHelper.GetMethod(components[0],
                  "System.Boolean", "Metodo per verificare la validità della classe.");
                mClass.Members.Add(mtd);

                List<CodeBinaryOperatorExpression> para =                     new List<CodeBinaryOperatorExpression>();
                int i = 1;
                while (i < components.Length - 1)
                {
                    string name = components[i];
                    i++;

                    string type = components[i];
                    i++;

                    CodeBinaryOperatorExpression ope = new CodeBinaryOperatorExpression();
                    ope.Left = CodeDomHelper.GetPlainCode(name);
                    ope.Operator = CodeBinaryOperatorType.IdentityInequality;
                    ope.Right = CodeDomHelper.GetPlainCode(type);
                    para.Add(ope);
                }

                CodeBinaryOperatorExpression retExp = new CodeBinaryOperatorExpression();
                if (para.Count > 1)
                {

                    retExp = para[0];
                    for (int j = 1; j < para.Count; j++)
                    {
                        CodeBinaryOperatorExpression tmpExp =                            new CodeBinaryOperatorExpression();
                                 tmpExp.Left = retExp;
                        tmpExp.Right = para[j];
                        tmpExp.Operator = CodeBinaryOperatorType.BooleanAnd;
                        retExp = tmpExp;
                    }

                }
                else
                {
                    retExp = para[0];
                }
                CodeMethodReturnStatement ret = new CodeMethodReturnStatement();
                ret.Expression = retExp;
                mtd.Statements.Add(ret);

            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                  System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + mClassName + 
		GlobalConstants.TXT_DOT
                    + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Il metodo IsValid, deve verificare che i dati della classe siano coerenti con uno standard da noi deciso, nel nostro esempio, abbiamo indicato tre diverse espressioni. Name deve essere diverso da null, FamilyName diverso da null e BirthDate diverso da DateTime.MinValue. Come per il metodo precedente, la prima cosa che viene fatta è generare un nuovo Method usando l’helper GetMethod e Aggiungere il Metodo generato alla collezione Methods dell’oggetto classe. Fatto questo, per ogni coppia di valori indicati sui dati della riga di comando, dobbiamo generare una espressione binaria, (Name != null – FamilyName != null – BirthDate != Datetime.MinValue). Per fare questo, utilizziamo un ciclo sulle stringhe dei parametri di comando a partire dal secondo elemento per ogni coppia di valori creiamo una CodeBinaryOperatorExpression, Questo oggetto fornisce tre proprietà, Espressione di sinistra,  Espressione di destra e Operatore. Inseriamo le 2 espressioni che abbiamo preparato a destra e a sinistra dell’operatore e indichiamo un operatore di tipo IdentityInequality ovvero diverso da. Fatto questo, aggiungiamo l’operatore binario ad una collezione che abbiamo predisposto allo scopo.

Terminato di preparare le espressioni di base, dobbiamo sommarle per ottenere l’espressione finale, infatti dobbiamo creare un espressione che esegue un AND logico fra le espressioni che abbiamo appena generato. Per farlo, creiamo un ulteriore CodeBinaryOperatorExpression, che chiamiamo retExp poi verifichiamo quanti operatori abbiamo nella collezione, se ce ne fosse uno solo, lo inseriremmo in retExp e proseguiremmo, se ve ne sono più di uno, effettuiamo un ciclo, poniamo il primo elemento della collezione nel nostro retExp, poi, a partire dal secondo elemento, creiamo un ulteriore CodeBinaryOperatorExpression in cui poniamo, a Sinistra ciò che si trova in retExp, a Destra l’espressione indicata dall’indice del ciclo e come operatore scegliamo un BooleanAnd, poi, inseriamo questa nuova espressione binaria in retExp. Proseguendo in questo modo, shiftiamo a sinistra una dopo l’altra le espressioni di base ed otteniamo un espressione composita. Alla fine del ciclo, creiamo una espressione di return, in cui inseriamo la nostra retExp ed aggiungiamo lo statement alla classe generata. Quello che sarà generato nella nostra classe è:

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

Il Metodo GenerateCompare

        private void GenerateCompare(string[] components)
        {
            try
            {
                CodeMemberMethod mtd = CodeDomHelper.GetMethod(components[0],
		 "System.Int32", "Metodo per comparare due classi.");
                mClass.Members.Add(mtd);
                mtd.Parameters.Add(CodeDomHelper.GetParameter(typeof(object), "obj"));

                mtd.Statements.Add(CodeDomHelper.GetLocalVariable("ret", typeof(int),
                    new CodePrimitiveExpression(-1)));

                CodeConditionStatement ifStat = new CodeConditionStatement();
                string condition = string.Empty;
                if (mLanguage == TargetLanguage.Cs)
                {
                    condition = string.Format("obj is {0}", mClass.Name);
                }
                else
                {
                    condition = string.Format("TypeOf obj Is {0}", mClass.Name);
                }
                ifStat.Condition = CodeDomHelper.GetPlainCode(
                    condition);

                CodeSnippetExpression val = null;
                if (mLanguage == TargetLanguage.Cs)
                {
                    val = CodeDomHelper.GetPlainCode(string.Format(                       "{0} val = ({0})obj", mClass.Name));
                }
                else
                {
                    val = CodeDomHelper.GetPlainCode(string.Format(                        "Dim val As {0} = DirectCast(obj, {0})", mClass.Name));
                }

                ifStat.TrueStatements.Add(val);

                mtd.Statements.Add(ifStat);

                List<KeyValuePair<string, string>> campi =                    new List<KeyValuePair<string, string>>();
                int i = 1;
                while (i < components.Length)
                {
                    KeyValuePair<string, string> kvp = new 
		KeyValuePair<string, string>(components[i], components[i + 1]);
                    i += 2;
                    campi.Add(kvp);
                }
                CodeConditionStatement ifInternal = ifStat;
                bool generateIndentIf = false;
                foreach (KeyValuePair<string, string> kvp in campi)
                {
                    if (generateIndentIf)
                    {
                        CodeConditionStatement newIf = new CodeConditionStatement();
                        newIf.Condition = new CodeBinaryOperatorExpression(
                            CodeDomHelper.GetPlainCode("ret"),
                            CodeBinaryOperatorType.IdentityEquality,
                            CodeDomHelper.GetValue(0));
                        ifInternal.TrueStatements.Add(newIf);
                        ifInternal = newIf;
                    }
                    if (kvp.Value == "date")
                    {
                        string code = string.Empty;
                        if (mLanguage == TargetLanguage.Cs)
                        {
                            code = "this.{0}";
                        }
                        else
                        {
                            code = "Me.{0}";
                        }
                        CodeMethodInvokeExpression call = CodeDomHelper.GetMethodCall(
                                                 string.Format(code, kvp.Key), "CompareTo",
                            new CodeExpression[] {CodeDomHelper.GetPlainCode(
                                string.Format("val.{0}", kvp.Value))});
                        ifInternal.TrueStatements.Add(CodeDomHelper.GetVariableAssignment(
                            "ret", call));
                        generateIndentIf = true;
                    }
                    else
                    {
                        string code = string.Empty;
                        if (mLanguage == TargetLanguage.Cs)
                        {
                            code = "StringHelper.Compare(this.{0}, val.{0})";
                        }
                        else
                        {
                            code = "StringHelper.Compare(Me.{0}, val.{0})";
                        }
                        ifInternal.TrueStatements.Add(CodeDomHelper.GetVariableAssignment(
                            "ret", CodeDomHelper.GetPlainCode(
                            string.Format(code, kvp.Key))));
                        generateIndentIf = true;
                    }
                }

                CodeMethodReturnStatement ret = new CodeMethodReturnStatement();
                ret.Expression = CodeDomHelper.GetPlainCode("ret");
                mtd.Statements.Add(ret);

            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                  System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + mClassName 
	       + GlobalConstants.TXT_DOT
                + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

Il metodo Compare, che ho predisposto per generare il codice come spiegato nelle entity base della serie Fritto Misto, è il più complesso di quelli già generati, infatti mentre i primi due contengono una sola riga con il return di una espressione, il compare effettua una serie di comparazioni indicando che due classi sono uguali quando tutti i loro membri pubblici elencati nei parametri di generazione sono uguali. Per fare questo, per prima cosa deve creare il metodo, fatto questo, lo aggiunge alla classe, dopodiché inizia a scrivere gli statement che lo compongono. La prima cosa che fa è creare il parametro di tipo object che è l’oggetto che sarà comparato all’oggetto in cui ci troviamo. Per effettuare questa operazione, aggiungiamo alla collection Parameters del metodo, un oggetto creato con il metodo GetParameter della classe Helper. Successivamente generiamo la variabile locale di tipo int che chiamiamo ret e la inizializziamo a -1, per farlo, usiamo la funzione helper GetLocalVariable, per aggiungere lo statement agli statement del nostro metodo.

Proseguendo nella generazione del codice del metodo, la prima cosa che dobbiamo verificare è se l’oggetto passato come parametro al metodo Compare è dello stesso tipo della classe in cui ci troviamo, se non fosse così Compare, per convenzione restituisce il -1 di default, ovvero qualsiasi oggetto che non è del tipo del mio oggetto è minore dell’oggetto stesso.

Per farlo, usiamo un po’ di BruteForce, questo perché non ho ancora rintracciato l’oggetto CodeDom per la creazione di uno statement di controllo del tipo pertanto, visto che produciamo sia C# che VB, inseriremo il codice in base al linguaggio impostato.

Creata l’espressione binaria per la if, proseguiamo generando l’espressione condizionale, per farlo produciamo un CodeConditionStatement,  in cui inseriamo l’espressione brute force generata e poi, per ognuna delle coppie di espressioni inserite nella riga di definizione del metodo, andiamo a generare un’ulteriore espressione condizionale interna alla “if” precedente. (in questo caso stiamo creando le matrioske interne alla IF principale). Inserite le IF concentriche per ognuna delle proprietà da comparare, creiamo un return statement per restituire il risultato della comparazione. Il codice del metodo che otterremo, è il seguente.

        /// <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;
        }

Con quest’ultimo metodo, abbiamo completato la creazione di ciò che sta dentro alla classe che vogliamo generare, adesso procediamo con la generazione vera e propria del file .cs o .vb della classe.

Il metodo GenerateClassCodeAndFile

        ///<summary>
        /// Genera la classe CSharp o VB
        ///</summary>
        private void GenerateClassCodeAndFile()
        {
            try
            {
                if (mLanguage == TargetLanguage.Cs)
                {
                    CodeDomHelper.GenerateCSharpCode(mOutFile, mUsingNamespace, mCodeNamespace);
                }
                else
                {
                    CodeDomHelper.GenerateVbCode(mOutFile, mUsingNamespace, mCodeNamespace);
                }
            }
            catch (Exception ex)
            {
                EventLogger.SendMsg(mClassName,
                    System.Reflection.MethodBase.GetCurrentMethod(), ex, MessageType.Error);
                throw new ApplicationException(GlobalConstants.TXT_SPACE + mClassName + 
		GlobalConstants.TXT_DOT
                    + System.Reflection.MethodBase.GetCurrentMethod().Name, ex);
            }
        }

La generazione del codice utilizzando Codedom è piuttosto semplice, anche se vedremo nel dettaglio i metodi nell’articolo dedicato alla nostra classe helper, per generare il codice passeremo alle fauci del compilatore il nome del file di output e i due oggetti namespace che custodiscono tutto quello che abbiamo preparato.

Fatto questo, andando a leggere il file di destinazione, potremo vedere una classe C# o VB con tutte le variabili, le proprietà ed i metodi che abbiamo deciso.

Conclusioni

La parte più difficile nella produzione del generatore di codice, non è scrivere i metodi che generano la classe, per quanto non sia proprio una operazione semplice. La parte più difficile è avere le idee chiare quando si progetta il generatore riguardo quello che è l’obiettivo e quali sono le funzionalità che la classe generata deve svolgere.

Nella Prossima parte della serie, spiegheremo, per quanto possibile come sono fatti i metodi usati per la creazione del generatore di Entity.

Il Progetto esempio con tutto il codice per i 3 articoli di questa serie può essere scaricato al link seguente:

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