Generování kódu pomoci T4 šablon, část 2

Tomáš Holan       10.06.2013             9999 zobrazení

V první části této série jsme se seznámili s tím jak vytvořit T4 šablonu, jaká je syntaxe T4 šablon a jaký je princip generování výstupního souboru na základě šablony. Také jsme zmínili, že výstupem šablony může být principiálně libovolný textový soubor (XML, .config soubory), ale my budeme uvažovat, využití T4 šablony pro generování zdrojového kódu. Nyní se blíže podíváme na to, co můžeme využít jako vstup T4 šablony.

Podle vstupu nebo zdroje, který v T4 šabloně používáme, bychom mohli způsob generování zdrojového kódu rozdělit na tři skupiny:

  • Externí soubor jako například XML dokument - Data pro generování kódu jsou umístěny v samostatném externím textovém nebo XML souboru nebo souborech, případně je použitý jiný externí zdroj (databáze, externí assembly). Kód šablony pak obsahuje logiku pro načtení a zpracování tohoto souboru. Externí soubor může být přidaný jako součást našeho VS projektu. V T4 šabloně se na něj pak lze odkázat například pomoci kódu:
string fileName = this.Host.ResolvePath("data.xml");
var xml = XDocument.Load(fileName).Root;

Také je nutné, aby v direktivě <#@ template #> šablony byl nastaven atribut hostSpecific="true", aby byla k dispozici vlastnost Host vracející objekt typu ITextTemplatingEngineHost.

Protože se jedná o poměrně jednoduchý a přímočarý případ, vice se mu zde věnovat nebudu.

  • Data jsou součástí vlastní T4 šablony - Potřebná data pro generování kódu jsou přímo součástí zdrojového kódu vlastní šablony. Přitom se může jednat například buď pouze o hodnoty konstant nebo nastavovaní vlastností nebo se může jednat o deklaraci pomocných tříd.
    V takovém případě lze hovořit o tom, že kód šablony se skládá ze dvou logických částí - dat a řídícího kódu šablony. Řídící kód šablony lze z důvodu jeho oddělení a možnosti znovu použitelnosti vyčlenit do samostatného souboru a v šabloně pro generování konkrétního souboru použít direktivu include.
    Tento způsob si v dnešní části ukážeme.
  • Třídy, které jsou součástí našeho projektu – Jako vstup slouží existující třídy, které jsou přímo součástí našeho projektu. T4 šablona je pak použita buď pro generování částí těchto tříd do samostatného souboru jako partial class v rámci stejného projektu nebo pro generování tříd do jiného projektu (např. generování klientských proxy tříd u Client-Server architektury).
    Tento scénář je velice užitečný, ale může být o něco složitější, budeme se mu proto věnovat v samostatné části této série.

Dnes si ukážeme příklad, kdy bude přímo součástí šablony uvedena deklarace tříd, které se použijí jako “vzor” pro vlastní generované třídy. Z každé vzorové třídy se použije pouze její název a deklarace její vlastnosti. Generovaná třída bude jednoduchá immutable třída, která bude obsahovat:

  • Deklarace vlastností jako veřejné, ale s privátním setrem.
  • Konstruktor pro inicializaci všech vlastností objektu.
  • Atribut DebuggerDisplay u deklarace třídy.

Dále si také ukážeme využití direktivy include pro oddělení konkrétní šablony od obecné šablony obsahující řídící kód tj. vlastní logiky generování.

Nutno ale říci, že toto řešení se nehodí na všechny scénáře, proto pod příkladem shrnu i některé jeho nevýhody.

Soubor T4 šablony pro generování konkrétních tříd bude mít název například Book.tt a bude vypadat následovně:

<#@ template debug="true" language="C#" #>
<#@ output extension=".generated.cs" #>
<#@ include file="ImmutableObjects.tt" #><#+
    public class Author
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Pseudonym { get; set; }
    }

    public class Book
    {
        public string Title { get; set; }
        public Author Author { get; set; }
        public string Category { get; set; }
        public int Year { get; set; }
    }
#>

Šablona obsahuje deklaraci “vzoru” pro generování třídy nebo tříd. Tato deklarace je umístěna v class feature bloku šablony.

V šabloně je použita direktiva include pro vložení samostatného souboru obecné T4 šablony nazvané ImmutableObjects.tt. Obsah tohoto souboru je následující:

<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
// ------------------------------------------------------------------------------
// This code was generated by template.
//
// Changes to this file will be lost if the code is regenerated.
// -----------------------------------------------------------------------------
using System;

namespace <#= System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint") #> 
{
<#
    this.PushIndent("\t");

    bool isFirstClass = true;
    foreach (var templateType in DiscoverTemplateTypes()) 
    {
        if (!isFirstClass)
        {
            WriteLine(null);
        }

        //Generování třídy
        var props = templateType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
#>
[System.Diagnostics.DebuggerDisplay("\\{ <#
        bool firstInSequence = true;
        foreach (var prop in props)
        {
            if (!firstInSequence) 
            { 
                Write(", "); 
            }
            Write(prop.Name);
            Write(" = {");
            Write(prop.Name);
            Write("}");
            firstInSequence = false;
        }
#> \\}")]
public partial class <#= templateType.Name #>
{
    #region member varible and default property initialization
<#
        //Generovaní jednotlivých vlastností
        foreach (var prop in props) 
        {
#>
    public <#= GetTypeName(prop.PropertyType) #> <#= prop.Name #> { get; private set; }
<#
        }
#>
    #endregion

<#
        //Kontruktor
#>
    #region constructors and destructors
    public <#= templateType.Name #>(<#
        firstInSequence = true;
        foreach (var prop in props)
        {
            if (!firstInSequence) 
            { 
                Write(", "); 
            }
            Write(GetTypeName(prop.PropertyType));
            Write(" ");
            Write(CamelCase(prop.Name));
            firstInSequence = false;
        }
#>)
    {
<#
        foreach (var prop in props) 
        {
#>
        this.<#= prop.Name #> = <#= CamelCase(prop.Name) #>;
<#
        }
#>
    }
    #endregion        
}
<#
        isFirstClass = false;
    }

    this.PopIndent();
#>
}<#+
    #region private member functions
    private IEnumerable<Type> DiscoverTemplateTypes()
    {
        return from type in this.GetType().Assembly.ExportedTypes
               where type.Name != "GeneratedTextTransformation"
               select type;
    }

    private static string GetTypeName(Type type) 
    {
        switch (type.Name)
        {
            case "String":
                return "string";
            case "Int16":
                return "short";
            case "Int32":
                return "int";
            case "Int64":
                return "long";
            case "Boolean":
                return "bool";
            case "Single":
                return "float";
            case "Double":
                return "double";
            case "Decimal":
                return "decimal";
            case "Byte[]":
                return "byte[]";
        }

        var nullableType = Nullable.GetUnderlyingType(type);
        if (nullableType != null)
        {
            return GetTypeName(nullableType) + "?";
        }

        if (type.IsGenericType)
        {
            var sb = new System.Text.StringBuilder();
            sb.Append(type.Name.Substring(0, type.Name.IndexOf("`")));
            sb.Append("<");

            bool isFirstArgument = true;
            foreach (Type arg in type.GetGenericArguments())
            {
                if (!isFirstArgument)
                {
                    sb.Append(",");
                }
                sb.Append(GetTypeName(arg));
                isFirstArgument = false;
            }

            sb.Append(">");
            return sb.ToString();
        }

        return type.Name;
    }

    private static string CamelCase(string name) 
    {
        if (name.StartsWith("ID", StringComparison.Ordinal))
        {
            return name;
        }

        return name.Substring(0,1).ToLowerInvariant() + name.Substring(1);
    }
    #endregion
#>

Protože tato šablona nemá generovat žádný vlastní výstup je potřeba u tohoto souboru nastavit vlastnost CustomTool na prázdnou hodnotu (tj. odmazat hodnotu “TextTemplatingFileGenerator”).

Vlastní generování třídy je poměrně jednoduché, protože lze použít reflection. Jak jsme uvedli minule “vzorové” třídy jsou totiž v době běhu řídícího kódu šablony zkompilované v pomocné assembly. Šablona tak generuje výstup pro všechny nalezené třídy v této assembly pouze z výjimkou třídy GeneratedTextTransformation vlastní šablony.

Výstup ze šablony Book.tt (soubor Book.generated.cs) bude konkrétně tento:

// ------------------------------------------------------------------------------
// This code was generated by template.
//
// Changes to this file will be lost if the code is regenerated.
// -----------------------------------------------------------------------------
using System;

namespace T4Sample2 
{
	[System.Diagnostics.DebuggerDisplay("\\{ FirstName = {FirstName}, LastName = {LastName}, Pseudonym = {Pseudonym} \\}")]
	public partial class Author
	{
	    #region member varible and default property initialization
	    public string FirstName { get; private set; }
	    public string LastName { get; private set; }
	    public string Pseudonym { get; private set; }
	    #endregion
	
	    #region constructors and destructors
	    public Author(string firstName, string lastName, string pseudonym)
	    {
	        this.FirstName = firstName;
	        this.LastName = lastName;
	        this.Pseudonym = pseudonym;
	    }
	    #endregion        
	}

	[System.Diagnostics.DebuggerDisplay("\\{ Title = {Title}, Author = {Author}, Category = {Category}, Year = {Year} \\}")]
	public partial class Book
	{
	    #region member varible and default property initialization
	    public string Title { get; private set; }
	    public Author Author { get; private set; }
	    public string Category { get; private set; }
	    public int Year { get; private set; }
	    #endregion
	
	    #region constructors and destructors
	    public Book(string title, Author author, string category, int year)
	    {
	        this.Title = title;
	        this.Author = author;
	        this.Category = category;
	        this.Year = year;
	    }
	    #endregion        
	}
}

Jak jsme viděli toho řešení je poměrně jednoduché, ale má některé vlastnosti, které mohou být někdy nevýhodné:

  • Definice “vzorové” třídy nebo tříd přímo součástí šablony není úplně elegantní, protože podpora editace kódu T4 šablony je i přes případné doplňky ve VS přece jenom stále na řádově horší úrovni jež přímá editace .cs souboru.
  • Protože negenerujeme pouze partial třídu, musíme generovat i členy, které byly již uvedené součástí deklarace vzorové třídy. Také, pokud do vzorové třídy uvedeme členy, se kterými šablona nepočítá, budou jednoduše ignorovány.
  • Pokud bychom potřebovali podporovat složitější deklarace vzorové třídy může být složitější i implementace transformační šablony.
  • Pokud by jsme chtěli do třídy doplňovat další členy, musíme je umístit již do třetího souboru – partial třídy ke generované třídě, což způsobí organizaci tříd ve VS projektu složitější.

Příště se podíváme na to, jak generovat kód podle existujících tříd v našem projektu.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback