What Is T4?
Well that’s a good question. No, it is not the fourth installment in the Terminator franchise; and it has nothing to do with C4. We are not talking about Terminal 4 or the SPARC T4. Instead we are talking about T4 being Text Template Transformation Toolkit. An uninspiring name to be sure, but what it lacks in a jazzy name, is made up for in cool functionality.
Maybe a better name would be ‘Insource Code Monkey’, because this is close to what it actually does. With T4 you can easily create and manipulate code templates to automate generating repetitive code; but the fun doesn’t stop there. As long as there is a pattern and a source of metadata we can generate text. It’s not just for code.
As wild and crazy as this may sound, if you use Visual Studio, you are using T4 probably without even knowing it. If you use Entity Framework, you are using T4 to generate entities. If you use MVC you use T4 when you select a scaffolding option.
You are also using it when you specify a Scaffold template for a strongly typed view.
If you are doing .Net programming and not using T4, your projects are probably taking longer than they should.
So What Can I Do With T4
T4 allows you to define templates that you can use to describe how generated code should look. With it, we can take as broad a definition of code and metadata as you want. By code we might mean C#, VB.Net, Html, JavaScript, SQL, PowerShell, CSS or PHP, for example. The metadata might come from Reflection, Database data dictionary, Configuration Data from a config file, User inputted data or Data from input prompts.
T4 allows you to define not just a static template but potentially a very dynamic template using your favorite language to iterate through loops or evaluate conditional logic.
Let’s consider a simple example. In the time-honored tradition of programming samples, here T4 says hello:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<#@ template debug="true" hostSpecific="true" #> <#@ output extension=".cs" #> <#@ Assembly id="SPAN" class=codeblue>"System.Core" #> <#@ Assembly id="SPAN" class=codeblue>"System.Windows.Forms" #> <#@ import namespace="System" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Diagnostics" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Collections" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Reflection" #> using System.Text; namespace T4Examples { public class FriendlyClass { public override string ToString () { var message = new StringBuilder (); <# var location = Path.GetDirectoryName(this.Host.TemplateFile) + (@"\bin\log4net.dll"); var assembly = Assembly.LoadFile (location); foreach (var type in assembly.GetTypes()) { if (type.IsInterface) { #> message.Append ("Hello <#= type.Name #> "); <# } } #> return message.ToString(); } } } |
When you run the generator, the code that I have marked in bold will be executed and incorporated into the generated code. Here we simply define a variable, message and then build a class that will display a shout out to all of the interfaces defined in the specified Assembly when the ToString method is called.
Don’t worry too much about the details in the template yet. All of this will make sense by the end of the article.
By the way, the generated code will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System.Text; namespace T4Examples { public class FriendlyClass { public override string ToString () { var message = new StringBuilder (); message.Append ("Hello IAppender "); message.Append ("Hello IBulkAppender "); message.Append ("Hello IOptionHandler "); message.Append ("Hello IXmlRepositoryConfigurator "); message.Append ("Hello IConvertTo "); ... return message.ToString(); } } } |
When Should I Use T4
Well it turns out this is not a trivial question.
Ideally our generator should require only a handful of inputs. If it requires too many, users may find it more useful to simply write the code by hand or find other ways to skip your generator.
We also have a couple of examples from Microsoft to follow on with ideas of how to incorporate code generation into our workflow. One approach is to preserve the generated code in its initial form so that the generator can be rerun on demand. With this approach we can ensure that our code stays in synch with the metadata, and stays up-to-date with best practices: As these change or the metadata changes, simply update the template and regenerate the code. This is how the Entity Framework uses T4. Repetitive tasks where the best practices may still be in flux are good candidates for this approach.
For this to work, we need to keep the generated code in a separate file from any custom code that we may add because T4 will completely replace the file each time. With this in mind, we can list out some firm recommendations:
- Always generate code as partial classes. Partial classes were introduced just for this purpose. We put our generated code in one file and make any customizations in the same class in a different file.
- Override methods in a derived class whenever possible so that the generated code is preserved.
- Add new properties or methods by updating the metadata. It is critical to keep the metadata up-to-date. Otherwise you may have problems the next time you generate code. If you manually add a property but someone else updates the metadata, you may get the property declared twice the next time you generate.
Another approach is to follow the lead from MVC. Here code generation is used only to get you started. You are expected to modify the generated code, but you are not expected to ever rerun the generator. Under this scenario, the code generator makes sure that you have a running start and don’t have to start from scratch, but you won’t ever regenerate the class. This simplifies the templates. They don’t have to be perfect since they are just a starting point. This also means that you can’t rely on the code generation to keep you in synch with changes to the metadata or best practices, but it can still be a huge boost to productivity.
Getting Started
Out of the Box, Visual Studio has minimal support for editing T4. You can manually create a .TT file and Visual Studio will treat it pretty much like a text file. There will be no syntax highlighting and no IntelliSense, but T4 will run your template. It will just be a pain to write.
Sadly this is the case even with Visual Studio 2012. To overcome this, there are a couple of extensions that can fill in the gaps. My preference is the Tangible Editor. They follow a Freemium business model where they provide a free version that has what you need to comfortably do light editing of a T4 template. You can follow the link above to compare the two versions. The most compelling reason to upgrade to the Pro version is being able to debug a template.
So in Visual Studio search the Extensions and Updates for Tangible T4 Editor and Download it.
This will download an MSI. Once the download is finished, exit Visual Studio and run the install.
Once you have finished, run the install, reopen Visual Studio and you will notice some obvious changes.
The template file that we looked at initially is much easier to read. The color coding helps.
You will also notice some new options when you add a new item.
Let’s start by adding a blank template. ‘Blank template’ is a bit of a misnomer. As we will see, while this template doesn’t actually do anything, it is not exactly blank.
The first line is the template directive. The debug attribute instructs the T4 engine to save generated files and debugging symbols in the TEMP folder (%USERPROFILE%\Local Settings\Temp). Among other things, this will allow you to see the file that gets created to implement the generator. You will generally want to keep this set to true.
The hostSpecific parameter instructs the engine that you want to integrate with the host. Usually this will be Visual Studio, but that is not the only option. By enabling this attribute, T4 will expose the Host property to your template. You can use this property to get more details about the environment that your script is running in. As we will see shortly, this opens up some intriguing possibilities for file management. The Host property is of type ITextTemplatingEngineHost so actually any class that implements this interface can be the host. While Visual Studio is a common host, it is not the only game in town.
The next line specifies the file extension for the file with the generated code. T4 will place all generated code in a file with this extension and with the same name as the original template. In Visual Studio, this file will be a child of the original template. The tangible editor also uses this to help with syntax highlighting for the code being generated in your template.
The next several lines refer to the code that will control the template execution. The Assembly directives are equivalent to adding a reference to a project. The import directives are similar to the using directive in C# or the Imports directive in VB.Net.
With this boilerplate in place, and a good deal more knowledge under our belt, let’s revisit our initial generator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<#@ template debug="true" hostSpecific="true" #> <#@ output extension=".cs" #> <#@ Assembly id="SPAN" class=codeblue>"System.Core" #> <#@ Assembly id="SPAN" class=codeblue>"System.Windows.Forms" #> <#@ import namespace="System" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Diagnostics" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Collections" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Reflection" #> using System.Text; namespace T4Examples { public class FriendlyClass { public override string ToString () { var message = new StringBuilder (); <# var location = Path.GetDirectoryName(this.Host.TemplateFile) + (@"\bin\log4net.dll"); var assembly = Assembly.LoadFile (location); foreach (var type in assembly.GetTypes()) { if (type.IsInterface) { #> message.Append ("Hello <#= type.Name #> "); <# } } #> return message.ToString(); } } } |
Looking over this template, we see that the template is going to use the System.Core assembly and will include 7 different namespaces. After T4’s needs are taken care of, we start building out what the generated code will need.
We specify that we are using the System.Text namespace and that our code will be in the T4Examples namespace. We explicitly name our generated class FriendlyClass and proceed to override the ToString() method. This is section is just raw C# code that will be copied unchanged into the generated code.
The next section, which I have marked in a bolder font, is the code for the template. Here we have a single input parameter, the name and location to a DLL that we will open with Reflection.
The template code will now Load the Assembly and loop through the types. If a type is actually an Interface, we will add a friendly greeting to the ToString() method.
Building on Our Initial Success
For our next trick, we will use reflection to find our ViewModels and stub out an editor view for each ViewModel that we find. We will follow the approach used with MVC itself and design a template that would be expected to run only once. It is really not realistic to expect a template to generate code that would never be modified directly. Also the finer details are not so easily predicted.
The first thing that we need to do is decide on how to identify the ViewModels. To keep things simply, we will define an empty interface IViewModel. As we loop through the types, we can easily identify these types with code like this:
1 2 3 4 5 6 7 8 |
<# var interfaceType = typeof (IViewModel); var types = interfaceType.Assembly.GetTypes (); foreach (var type in types) { if (interfaceType.IsAssignableFrom (type)) { #> |
This snippet of code exploits the relationship that the ViewModels are in the same assembly where the IViewModel is stored. If this is not true in your scenario, you will need to load the Assembly with the ViewModels separately.
Now that we can easily identify our ViewModels, we can turn our attention to what the editors should look like. Initially we can start by simply wrapping a DIV tag around a group of paragraphs one for each property in the associated Model. Our template may look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@model <#= type.FullName #> <div class = "editor"> <# foreach ( var property in type.GetProperties()) { #> <p> <label><#= property.Name #> </label> @Html.EditorFor(model => model.<#= property.Name #>) <p> <# } // For each #> </div> <# } // end if IViewModel } // End looping through the types #> |
This template will produce code that looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@model IViewModel <div class = "editor"> </div> @model ClosingFollowupModel <div class = "editor"> <p> <label>Data </label> @Html.EditorFor(model => model.Data) </p> <p> <label>ClosingDate </label> @Html.EditorFor(model => model.ClosingDate) </p> <p> <label>Filters </label> @Html.EditorFor(model => model.Filters) </p> </div> @model ErrorModel <div class = "editor"> <p> <label>ErrorTitle </label> @Html.EditorFor(model => model.ErrorTitle) </p> <p> <label>ExceptionDetail </label> @Html.EditorFor(model => model.ExceptionDetail) </p> </div> @model RecordFilter <div class = "editor"> <p> <label>Operations </label> @Html.EditorFor(model => model.Operations) </p> <p> <label>SelectedOperations </label> @Html.EditorFor(model => model.SelectedOperations) </p> <p> <label>Servicers </label> @Html.EditorFor(model => model.Servicers) </p> </div> |
One thing that I don’t like about T4 is that the code is stored in one file nested under the template. I like to have a separate file for each class. I think it is better practice to have each class in a file by itself and name this file the same as the class. A great advantage of doing it this way is that these individual editors must be in separate files for MVC to find them. Not only that, they cannot be nested under the template itself. They need to be stored in the Shared/EditorTemplates folder.
While T4 doesn’t natively want to do this, we can make it work with a little bit of fancy footwork. Damien Guard has worked out the fancy footwork and packaged it all up in a Manager class that we can include in our templates.
We can easily incorporate this into our simple template.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<#@ template debug="true" hostSpecific="true" #> <#@ output extension=".cshtml" #> <#@ Assembly id="SPAN" class=codeblue>"System.Core" #> <#@ import namespace="System" #> <#@ import namespace="System.Collections.Generic" #> <#@ include file="Databases.ttinclude" #> <#@ include file="Manager.ttinclude" #> <#@ include file="Manager.ttinclude" #> <# var interfaceType = typeof (IViewModel); var types = interfaceType.Assembly.GetTypes (); var manager = Manager.Create(Host, GenerationEnvironment); foreach (var type in types) { if (interfaceType.IsAssignableFrom (type)) { manager.StartNewFile(type.Name +".cshtml", Path.GetDirectoryName(Host.TemplateFile) + @"\Views\Shared\EditorTemplates"); #> @model <#= type.Name #> <div class = "editor"> <# foreach ( var property in type.GetProperties()) { #> <p> <label><#= property.Name #></label> @Html.EditorFor(model => model.<#= property.Name #>) </p> <# } // For each property #> </div> <#= manager.EndBlock(); } // end if IViewModel } // End looping through the types manager.Process (true); #> |
The first change to note is that we have included Damien’s Manager Class. The next big change comes when we create a manager. Host will refer to Visual Studio andGenerationEnvironment is a StringBuilder that T4 uses to build the generated code.
As we loop through the ViewModels we will create a new partial view after we instruct the manager to start a new file. We use the Host to find out where the template itself is located, and build the path from there. If you place your templates in the project root folder, you can easily build out whatever directory structure you want. Here, in this example, we want the generated code to be placed in a specific location and be a part of the shared views.
After we are finished with generating the view, we instruct the manager that we are through with that file. Once we have processed all ViewModels, we will call the Process method. This is where the magic takes place to actually create the files, and then adding them to the project. In the end, we will have a partial view for each ViewModelin the shared EditorTemplates folder.
This approach allows you to place the generated code in more predictable places in your project, not just as a child of the template. From a change control perspective, you can better track what is actually changing over time, and you can easily remove the templates if they are no longer needed.
This strategy also relies on being able to easily make changes to the generated code without losing them the next time that we run the code generator. To make this work, we will add a simple check to see if the generated file exists. If it does, we will skip the ViewModel. If the file does not already exist, we will generate it based on the current code template. If the file is missing, we will go ahead and create it.
This will ensure that the generator runs only one time, unless you explicitly delete the file. Here are the key pieces that need to change:
1 2 3 4 5 6 7 8 9 10 |
foreach (var type in types) { if (interfaceType.IsAssignableFrom (type)) { var path = Path.GetDirectoryName(Host.TemplateFile) + @"\Views\Shared\EditorTemplates"; if (File.Exists (Path.Combine (path, type.Name + ".cshtml"))) continue; manager.StartNewFile(type.Name +".cshtml" ,path ); |
Conclusion
T4 is a powerful tool to have in your toolbox. In any project, there will be repetitive code that you don’t want to have to code by hand each time. There are also many examples of volatile code where the best practices change quickly and often. Code generation with T4 makes it easy to automate such tasks and improve your delivery dates.
If you are not currently using T4, you owe it to yourself to give it a try.
Load comments