Freigeben über


VS2010 SP1: T4 Template Inheritance Part II – The Core Template

 

Last time, I outlined my scenario – we have a template that produces a very vanilla C# class from metadata and we’d like to customize it to produce something more directly applicable to our current project.  Of course, we could always just copy the standard template into our project and hack around a little until it meets our needs.  With simple templates, that’s often the right thing to do, however once you start to do a lot of code generation, copy/paste reuse just doesn’t cut it and you need some structure.  You can find my complete sample attached to this post.

The first thing I did was to switch to precompiled templates (introduced in Visual Studio 2010) for my core class-generating template.  A precompiled template gives me a class that you can instantiate and then call the TransformText() method on. This method will then produce the same output as a regular template and you can use a simple harness to call it and write the output to disk.  I’m using a regular T4 template as my harness because it’s the easiest way to get a string written to disk as a file in the project system inside Visual Studio.  You can add a preprocessed T4 template from the Visual Studio 2010 Project/Add New Item dialog.

Here’s the regular harness template, using a preprocessed template class called Book, generated from a preprocessed template called Book.tt

    1: <#@ template debug="false" hostspecific="false" language="C#" #>
    2: <#@ output extension=".cs" #>
    3: <#@ assembly name="$(SolutionDir)Templates\$(OutDir)Templates.dll" #>
    4: <#@ assembly name="$(SolutionDir)BaseTemplates\$(OutDir)BaseTemplates.dll" #>
    5: <#@ Import namespace="Templates" #>
    6: <#@ Include File="$(SolutionDir)BaseTemplates\Helpers.t4" #>
    7: namespace TemplateUse
    8: {
    9:     using System;
   10: <#
   11:     var book = new Book();
   12:     this.PushIndent();
   13:     this.WriteLine(book.TransformText());
   14:     this.PopIndent(); #>
   15: }

I generally put preprocessed templates in a separate project/assembly as you don’t usually want them leaking into your actual application. Here you can see I’ve put them in the Templates and BaseTemplates projects which I’m referring to via VS 2010’s new ability to reference assemblies via project macros such as $(SolutionDir).  This style of working is much, much easier now with Visual Studio 2010 SP1, as we’ve fixed the assembly locking issues that meant you had to keep restarting your IDE.

I’m using a helper function from my standard function library (Helpers.t4) to make indenting the code a bit more standardized and customizable – I’ll come back to that as a customization point in another post.  The main thrust of this wrapper is that it simply uses WriteLine() to spit out the code from the preprocessed template class.

 

So what’s in the Book class? Let’s have a look at the preprocessed template that generated it:

    1: <#@ template debug="false" hostspecific="false" language="C#" inherits="BaseTemplates.DataClass" #>
    2: <#@ assembly name="$(SolutionDir)\BaseTemplates\bin\debug\BaseTemplates.dll" #>
    3: <#@ import namespace="BaseTemplates" #>
    4: <#@ output extension=".cs" #>
    5: <#
    6: // Copyright (C) Microsoft Corporation.  All rights reserved.
    7: this.Description = new TypeDescription
    8:             {
    9:                 Name="Book",  Description="A class to carry data about a book in a library system.",
   10:                 Properties=
   11:                 {
   12:                     new TypePropertyDescription{Name="Title",         Type="string",    Description="The title of the book."},
   13:                     new TypePropertyDescription{Name="AuthorID",      Type="string",    Description="The ID of the author of the book."},
   14:                     new TypePropertyDescription{Name="ISBN",          Type="string",    Description="The standard ISBN number of the book."},
   15:                     new TypePropertyDescription{Name="CopiesOwned",   Type="int",       Description="The number of copies the library owns."}
   16:                 }
   17:             };
   18: base.TransformText();
   19: #>

You can see straightaway that this is mostly just the metadata set-up we described last time.  All of the real work is being done in the base template that’s referenced in the <#@ template inherits=BaseTemplates.DataClass” #> directive.  That template defines a contract (in the loosest sense of the word) that says you have to set its Description property to a piece of metadata before calling the base’s TransformText() and it will generate a matching class for you. Here it is:

    1: <#@ template language="C#" #>
    2: <#@ import namespace="System.Collections.Generic" #>
    3: <#@ include file="CSharpHelpers.t4" #><#
    4: // -----------------------------------------------------------------------------
    5: // Template to create a simple C# data carrier class from an instance of datatype description metadata
    6: // -----------------------------------------------------------------------------
    7:  
    8: if (this.Description == null)
    9: {
   10:     this.Error("Set the Description property on this template to an instance of the class TypeDescription.");
   11: }
   12: else
   13: {
   14:     // Define the overall structure of a class.
   15:     // Generally this should have no template boilerplate in it, so the structure is clear and simple.
   16:     Summary(this.Description.Description);
   17:     ClassHeader(this.Description);
   18:     OpenBrace();
   19:     foreach (var property in this.Description.Properties) 
   20:     {
   21:         Property(property);
   22:         NewLine();
   23:     }
   24:     CloseBrace();
   25: }
   26: #>
   27: <#+ 
   28:  
   29: // Control properties that drive the template
   30: public TypeDescription Description { get; set; } // Description that this template uses.
   31:  
   32: // -----------------------------------------------------------------------------
   33: // Template snippet functions for the individual pieces of a class
   34: // Snippets are not indented at all - it is the responsibility of the calling structural method to set correct indenting.
   35: // -----------------------------------------------------------------------------
   36:  
   37: /// <summary>
   38: /// Generation method to define the template snippet for a summary comment
   39: /// </summary>
   40: public virtual void Summary(string comment)
   41: {
   42: #>
   43: /// <summary>
   44: /// <#= comment #>
   45: /// </summary>
   46: <#+ 
   47: }
   48:  
   49: /// <summary>
   50: /// Generation method to define the template snippet for a property declaration
   51: /// </summary>
   52: public virtual void Property(TypePropertyDescription property)
   53: {
   54:     Summary(property.Description);
   55: #>
   56: public <#= property.Type #> <#= property.Name #> { get; set; }
   57: <#+ 
   58: }
   59:  
   60: /// <summary>
   61: /// Generation method to define the template snippet for the declaration line of a class
   62: /// </summary>
   63: public virtual void ClassHeader(TypeDescription type)
   64: {
   65: #>
   66: public class <#= type.Name #>
   67: <#+ 
   68: }
   69: #>

The key here is structure. The template has an initial control block that validates its metadata and then defines the logic for generating a class from the metadata provided.  That logic is written as ordinary procedural code and has no template boilerplate in it at all.  I find this separation of core control logic from output text to be key in keeping larger templates maintainable.  The control code uses a few simple helpers (OpenBrace, CloseBrace, NewLine) to avoid clumsy WriteLine statements and literal strings. This could form the start of a language-agnostic approach to structuring templates, although I’m not going to go down that path here.

The control code then calls out to various methods defined in class feature blocks that write snippets of the output using standard boilerplate syntax. Notice that each of these methods is defined as a virtual method.  Here we have the key to our extensibility story.  We can derive from this template class and replace just the pieces of output generation that we want without disturbing any others or the core logic. In fact, it’s probably a good idea to move that core logic into a virtual method of its own as well so it can be independently customized without disturbing the snippets.

Notice that none of the ouput snippet methods do any indenting – they all assume zero external indentation and leave their callers to manage it.  That way you can build up a library of output methods and mix and match them from different pieces of control logic. You could also break down their parameters to only use simple types so they were independent of specific forms of metadata – it’s a balance between readability and reuse here.

 

This time, we’ve seen how the core template works to generate vanilla class code in a structured, extensible manner.  Next time we’ll look at taking advantage of that extensibility to create the custom class we asked for in Part I.

 

 

Technorati Tags: T4,Visual Studio 2010 SP1