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

Tomáš Holan       24.06.2013             10286 zobrazení

V předchozích částech této série jsme zatím používali T4 šablonu ke generování jednoho výstupního souboru, což je také výchozí způsob. Při něm je název výstupního souboru odvozen přímo od názvu souboru šablony a má jen jinou příponu, kterou lze určit direktivou <#@ output #>. Někdy se nám ale může hodit generovat jednou T4 šablonou výstupních souborů několik. Dnes upravíme příklad z minula tak, aby se každá generovaná partial třída uložila do samostatného souboru.

Tento úkol má vlastně dvě části:

  • Doplnit kód, který provede vytvoření jednotlivých výstupních souborů.
    a
  • Zabránit generování výchozího výstupního souboru T4 šablony.

Začneme nejprve druhým bodem. Protože infrastruktura pro zpracování transformace T4 šablony moc nepočítá s tím, že by jsme nechtěli výstupní soubor generovat, je potřeba použít trochu ošklivý trik.

Pokud nastavíme direktivu <#@ output #> takto:

<#@ output extension="\\" #>

Nepůjde výstupní soubor založit a proto jednoduše při transformaci šablony nevznikne. Drobnou nevýhodou bude výpis warningu:

“Unable to write the output of custom tool ‘TextTemplatingFileGenerator’ to file …”

Kromě něho ale transformace bude jinak probíhat korektně (až doplníme kód pro vytváření výstupních souborů). Jiným řešením by bylo změnit příponu výstupního souboru na .txt a zapsat do něho například seznam ostatních generovaných souborů. Které řešení je lepší nechám na vás.

Pro generování výstupních souborů použijeme objekt EntityFrameworkTemplateFileManager. K tomu, aby byl tento objekt dostupný, nám stačí do T4 šablony přidat následující direktivu <#@ include #>:

<#@ include file="EF.Utility.CS.ttinclude" #>

Odkazovaný soubor EF.Utility.CS.ttinclude je vždy po instalaci Visual Studia 2012 dostupný, protože je to součást podpory generování kódu pomoci T4 pro Entity Framework. Tento soubor se konkrétně nachází v adresáři:

c:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\Extensions\Microsoft\Entity Framework Tools\Templates\Includes

Dále musí být ještě v direktivě <#@ template #> šablony nastaven atribut hostSpecific="true".

Pro založení nového výstupního souboru pak stačí na vytvořeném objektu typu EntityFrameworkTemplateFileManager volat metodu StartNewFile s názvem daného souboru. Tím se veškerý další výstup automaticky “přesměruje” do tohoto souboru. Na konci celého zpracování musíme ještě zavolat metodu Process. Ukazuje to následují jednoduchá ukázka:

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ output extension="\\" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#
  var fileManager = EntityFrameworkTemplateFileManager.Create(this);

  for (int i = 1; i < 4; i++) 
  {
    fileManager.StartNewFile("file" + i.ToString() + ".cs");
#>
//Here is some content for file<#= i.ToString() #>.cs
<#
  }

  fileManager.Process();
#>

Všechny založené výstupní soubory (v ukázce výše to jsou file1.cs, file2.cs a file3.cs) budou ve VS projektu zobrazovány pod souborem T4 šablony.

T4 šablona z příkladu z minula upravená tak, aby byl pro každou partial třídu vytvořen samostatný soubor, bude vypadat takto:

<#@ template language="C#" hostSpecific="true" debug="true" #>
<#@ output extension="\\" #>
<#@ include file="EF.Utility.CS.ttinclude" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Collections.Generic" #>
<#
    var fileManager = EntityFrameworkTemplateFileManager.Create(this);

    var classes = ProjectInspector.FindClassesInNamespace(this.Host, (string)System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint"));
    foreach (var classInfo in classes)
    {
        fileManager.StartNewFile(classInfo.Name + ".generated.cs");

        //Generování třídy
#>
// ------------------------------------------------------------------------------
// This code was generated by template.
//
// Changes to this file will be lost if the code is regenerated.
// ------------------------------------------------------------------------------
using System;
using System.Collections.Generic;

namespace <#= System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint") #> 
{
    [System.Diagnostics.DebuggerDisplay("\\{ <#
        bool firstInSequence = true;
        foreach(var prop in classInfo.Properties)
        {
            if (!firstInSequence) 
            { 
                Write(", "); 
            }
            Write(prop.Name);
            Write(" = {");
            Write(prop.Name);
            Write("}");
            firstInSequence = false;
        }
#> \\}")]
    partial class <#= classInfo.Name #>
    {
<#
        //Kontruktor
#>
        #region constructors and destructors
        public <#= classInfo.Name #>(<#
        firstInSequence = true;
        foreach(var prop in classInfo.Properties)
        {
            if (!firstInSequence) 
            { 
                Write(", "); 
            }
            Write(prop.PropertyType);
            Write(" ");
            Write(CamelCase(prop.Name));
            firstInSequence = false;
        }
#>)
        {
<#
        foreach(var prop in classInfo.Properties) 
        {
#>
            this.<#= prop.Name #> = <#= CamelCase(prop.Name) #>;
<#
        }
#>
        }
        #endregion
    }
}<#
    }

    fileManager.Process();
#>
<#+
    #region member types definition
    private static class ProjectInspector
    {
        #region action methods
        public static IEnumerable<ClassInfo> FindClassesInNamespace(Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost host, string targetNamespace)
        {
            IServiceProvider hostServiceProvider = (IServiceProvider)host;
            EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
            EnvDTE.ProjectItem containingProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
            Project project = containingProjectItem.ContainingProject;

            var classes = new Dictionary<string, ClassInfo>();
            ProcessProjectItem(classes, project.ProjectItems, targetNamespace);
   
            return classes.Values;
        }
        #endregion

        #region private member functions
        private static void ProcessProjectItem(IDictionary<string, ClassInfo> classes, ProjectItems projectItems, string targetNamespace)
        {
            foreach (ProjectItem projectItem in projectItems)
            {
                FileCodeModel fileCodeModel = projectItem.FileCodeModel;
                if (fileCodeModel != null)
                {
                    foreach (CodeElement codeElement in fileCodeModel.CodeElements)
                    {
                        WalkElements(classes, codeElement, null, targetNamespace);
                    }
                }

                if (projectItem.ProjectItems != null)
                {
                    ProcessProjectItem(classes, projectItem.ProjectItems, targetNamespace);
                }
            }
        }

        private static void WalkElements(IDictionary<string, ClassInfo> classes, CodeElement codeElement, ClassInfo currentClass, string targetNamespace)
        {
            switch (codeElement.Kind)
            {
                case vsCMElement.vsCMElementNamespace:
                {
                    //Namespace
                    EnvDTE.CodeNamespace codeNamespace = (EnvDTE.CodeNamespace)codeElement;
                    if (codeNamespace.FullName == targetNamespace)
                    {
                        foreach (CodeElement element in codeNamespace.Members)
                        {
                            WalkElements(classes, element, null, targetNamespace);
                        }
                    }
                    break;
                }
                case vsCMElement.vsCMElementClass:
                {
                    //Class
                    EnvDTE.CodeClass codeClass = (EnvDTE.CodeClass)codeElement;

                    ClassInfo classInfo;
                    if (!classes.TryGetValue(codeClass.Name, out classInfo))
                    {
                        IEnumerable<string> attributes = null;
                        if (codeClass.Attributes != null)
                        {
                            attributes = from CodeAttribute a in codeClass.Attributes
                                         select a.Name;
                        }
                        classInfo = new ClassInfo(codeClass.Name, attributes);
                        classes.Add(classInfo.Name, classInfo);
                    }

                    if (codeClass.Members != null)
                    {
                        foreach (CodeElement element in codeClass.Members)
                        {
                            WalkElements(classes, element, classInfo, targetNamespace);
                        }
                    }
                    break;
                }
                case vsCMElement.vsCMElementProperty:
                {
                    //Property
                    EnvDTE.CodeProperty codeProperty = (EnvDTE.CodeProperty)codeElement;
                    if (codeProperty.Name != "this" && codeProperty.Setter != null)
                    {
                        IEnumerable<string> attributes = null;
                        if (codeProperty.Attributes != null)
                        {
                            attributes = from CodeAttribute a in codeProperty.Attributes
                                         select a.Name;
                        }

                        string propertyType = codeProperty.Type.AsString;
                        if (propertyType.StartsWith(targetNamespace + ".", StringComparison.Ordinal))
                        {
                            propertyType = propertyType.Substring((targetNamespace + ".").Length);
                        }

                        currentClass.AddProperty(new ClassPropertyInfo(codeProperty.Name, propertyType, attributes));
                    }
                    break;
                }
            }
        }
        #endregion
    }

    private sealed class ClassInfo
    {
        #region member varible and default property initialization
        public string Name { get; private set; }
        private readonly List<ClassPropertyInfo> m_Properties = new List<ClassPropertyInfo>();
        public HashSet<string> Attributes { get; private set; }
        #endregion

        #region constructors and destructors
        public ClassInfo(string name, IEnumerable<string> attributes)
        {
            this.Name = name;
            if (attributes == null)
            {
                this.Attributes = new HashSet<string>();
            }
            else
            {
                this.Attributes = new HashSet<string>(attributes);
            }
        }
        #endregion

        #region action methods
        public void AddProperty(ClassPropertyInfo prop)
        {
            m_Properties.Add(prop);
        }   
        #endregion

        #region property getters/setters
        public IList<ClassPropertyInfo> Properties
        {
            get { return m_Properties.AsReadOnly(); }
        }
        #endregion
    }

    private sealed class ClassPropertyInfo
    {
        #region member varible and default property initialization
        public string Name { get; private set; }
        public string PropertyType { get; private set; }
        public HashSet<string> Attributes { get; private set; }
        #endregion

        #region constructors and destructors
        public ClassPropertyInfo(string name, string propertyType, IEnumerable<string> attributes)
        {
            this.Name = name;
            this.PropertyType = propertyType;
            if (attributes == null)
            {
                this.Attributes = new HashSet<string>();
            }
            else
            {
                this.Attributes = new HashSet<string>(attributes);
            }

            if (this.PropertyType.StartsWith("System.", StringComparison.Ordinal))
            {
                this.PropertyType = this.PropertyType.Substring("System.".Length);
            }
            if (this.PropertyType.StartsWith("Collections.Generic.", StringComparison.Ordinal))
            {
                this.PropertyType = this.PropertyType.Substring("Collections.Generic.".Length);
            }
        }
        #endregion
    }
    #endregion

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

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

Výstupem pak bude soubor Author.generated.cs:

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

namespace T4Sample4 
{
    [System.Diagnostics.DebuggerDisplay("\\{ FirstName = {FirstName}, LastName = {LastName}, Pseudonym = {Pseudonym} \\}")]
    partial class Author
    {
        #region constructors and destructors
        public Author(string firstName, string lastName, string pseudonym)
        {
            this.FirstName = firstName;
            this.LastName = lastName;
            this.Pseudonym = pseudonym;
        }
        #endregion
    }
}

a soubor Book.generated.cs:

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

namespace T4Sample4 
{
    [System.Diagnostics.DebuggerDisplay("\\{ Title = {Title}, Author = {Author}, Category = {Category}, Year = {Year} \\}")]
    partial class Book
    {
        #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
    }
}

Ještě uvedu některé odkazy na další zdroje, které se zabývají T4 šablonami a generováním kódu:

http://msdn.microsoft.com/en-us/library/vstudio/bb126445.aspx
http://www.olegsych.com/2007/12/text-template-transformation-toolkit
http://www.olegsych.com/2008/05/t4-architecture
http://www.olegsych.com/2008/03/code-generation-with-visual-studio-templates
http://msdn.microsoft.com/en-us/magazine/hh882448.aspx
http://msdn.microsoft.com/en-us/magazine/hh975350.aspx
http://msdn.microsoft.com/en-us/data/gg558520.aspx
http://www.olegsych.com/2008/07/t4-template-for-generating-sql-view-from-csharp-enumeration
http://msdn.microsoft.com/en-us/library/k3dys0y2.aspx
http://visualstudiogallery.msdn.microsoft.com/b0e2dde6-5408-42c2-bc92-ac36942bbee9

 

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