V minulé části této série jsme ukázali příklad generování třídy podle “vzorové” třídy, která byla umístěna přímo součástí vlastní T4 šablony. Často by se mám ale mohlo hodit použít jako vzor již existující třídy, které jsou součástí našeho projektu. Toto je využitelné ve dvou scénářích:
- Dogenerovaní některých dalších členů k existující třídě tj. generování partial třídy v rámci stejného projektu jako je vzorová třída.
- Generování tříd do jiného projektu než jsou vzorové třídy. Může se jednat například o klientské proxy třídy u Client-Server architektury aplikace. V tomto případě bude T4 šablona umístěna v cílovém VS projektu.
Analogicky k příkladu z minula by to v prvním případě mohlo vypadat tak, že napíšeme deklaraci třídy pouze s jejími vlastnostmi a T4 šablona k ní vygeneruje například konstruktor a atribut DebuggerDisplay. Tento způsob si ukážeme v dnešním příkladu.
Minule jsme pro “analýzu” vzorové třídy použili reflection, což činilo situaci poměrně jednoduchou. Jde ale použít reflection i pro analýzu existující třídy, která je součástí VS projektu nebo solution? Aby šlo reflection použít musí být analyzovaná třída již zkompilována v assembly, a to přináší hned dva problémy:
- Před spuštěním generování T4 šablony by jsme museli aktuální projekt vždy explicitně zkompilovat.
a
- Při generování výstupu T4 šablony by se tato assembly načetla, ale již neuvolnila, což by způsobilo, že aktuální projekt by již nešlo znovu zkomplikovat dokud neuzavřeme a znovu otevřeme Visual Studio.
Z těchto důvodů zde není reflection příliš vhodná. Pro tento scénář potřebujeme něco, co umí procházet aktuální projekt otevřený ve VS a umí zpřístupnit definice z kódu přímo z jeho textového zápisu v souborech projektu.
Aktuálně lze pro toto využít knihovnu Code Model, která je součástí Visual Studia. Objekty, které tato knihovna definuje se nachází v namespace EnvDTE. Nutno ale hned upozornit, že psát kód oproti této knihovně není úplně jednoduchá záležitost. To je důsledek hlavně těchto skutečností:
- Objekty knihovny jsou wrappery nad COM objekty.
- Vlastní knihovna a tedy i návrh jejího API je poměrně starý a není příliš “.net friendly”.
- Knihovna nezahrnuje specifické vlastnosti .NET programovacích jazyků, proto jsou možnosti knihovny omezené.
- Oficiální dokumentace ke knihovně není příliš podrobná a obecně zdrojů ohledně použití této knihovny je jen velmi málo.
A to právě činí složitost celého řešení v porovnání s příkladem z minula řádově vyšší.
Toto je oblast, kde bude do budoucna využitelný projekt s kódovým názvem Roslyn. Jedná se o kompilátor jazyka C# (a Visual Basic .NET) zpřístupněný v podobě zcela nového veřejně dostupného API. Toto API pak umožňuje syntaktickou i sémantickou analýzu zdrojového kódu stejně tak jako různé manipulace, transformace nebo jiné zpracování nad sestaveným syntaktickým stromem. To primárně umožní implementovat záležitosti jako nástroje pro refactoring nebo statickou analýzu kódu a také právě generování kódu.
Nyní již samotný příklad. Nejprve si přímo do projektu přidáme soubor Book.cs s deklarací partial tříd, ke kterým budeme chtít šablonou generovat další členy:
using System;
namespace T4Sample3
{
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
}
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
}
}
V příkladu T4 šablony budeme provádět vyhledání všech tříd v určeném namespace. Tímto namespace bude namespace odpovídající umístění souboru s T4 šablonou ve VS projektu (namespace vracený jako "NamespaceHint") tak, aby stačilo zdrojové třídy a šablonu umístit do stejného podadresáře. Ke všem nalezeným třídám pak šablona provede generování partial třídy.
Zdrojový kód T4 šablony bude:
<#@ template language="C#" hostSpecific="true" debug="true" #>
<#@ output extension=".generated.cs" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Collections.Generic" #>
// ------------------------------------------------------------------------------
// 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") #>
{
<#
this.PushIndent("\t");
var classes = ProjectInspector.FindClassesInNamespace(this.Host, (string)System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint"));
bool isFirstClass = true;
foreach (var classInfo in classes)
{
if (!isFirstClass)
{
WriteLine(null);
}
//Generování třídy
#>
[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
}
<#
isFirstClass = false;
}
this.PopIndent();
#>
}<#+
#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ýstup této šablony pak bude:
// ------------------------------------------------------------------------------
// 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 T4Sample3
{
[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
}
[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
}
}
Příště: Dnešní příklad upravíme tak, aby se jednotlivé generované partial třídy umístili do samostatných souborů.
(*) V době psaní tohoto článku v September 2012 CTP.