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