Multiple outputs from T4 made easy

January 2009 – November 2009 .NET () • 10,690 views • 14 responses

An improved version is now available.

One of the things I wanted my LINQ to SQL T4 templates to do was be able to split the output into a file-per-entity. Existing solutions used either a separate set of templates with duplicate code or intrusive handling code throughout the template. Here’s my helper class to abstract the problem away from what is already complicated enough template code.

Using the Manager class

Setup

You’ll need to get the code into your template – either copy the code in or reference it with an include directive. Then declare an instance of the Manager class passing in some environmental options such as the desired default output path.

<#@ template language="C#v3.5" hostspecific="True"
#><#@ include file="Manager.ttinclude"
#><# var manager = new Manager(Host, GenerationEnvironment, true) { OutputPath = Path.GetDirectoryName(Host.TemplateFile) }; #>

Define a block

Then add one line before and one line after each block which could be split out into it’s own file passing in what the filename would be if split.

<# manager.StartBlock("Employee.generated.cs"); #>
public class Employee { … }
<# manager.EndBlock(); #>

Headers and footers

Many templates need to share a common header/footer for such things as comments or using/import statements or turning on/off warnings. Simply use StartHeader/EndHeader and StartFooter/EndFooter. The resulting blocks will be emitted into all split files and left in the original output too.

<# manager.StartHeader(); #>
// Code generated template
using System;

<# manager.EndHeader(); #>

Process

At the end of the template call Process to handle splitting the files (true) or not (false). Anything not included in a specific start/end block will remain in the original output file.

<# manager.Process(true); #>

When processing each block name in the Output path will either be overwritten or deleted to enable proper clean-up. It will also add and remove the files from Visual Studio so make sure your generated names aren’t going to collide with hand-written ones!

Manager classes

Here is the Manger class itself as well as the small ManagementStrategy classes that determines what to do with the files within Visual Studio (add/remove project items) and outside of Visual Studio (create/delete files).

Download Manager.ttinclude (4KB)

<#@ assembly name="System.Core"
#><#@ assembly name="EnvDTE"
#><#@ import namespace="System.Collections.Generic"
#><#@ import namespace="System.IO"
#><#@ import namespace="System.Text"
#><#@ import namespace="Microsoft.VisualStudio.TextTemplating"
#><#+

// T4 Template Block manager for handling multiple file outputs more easily.
// Copyright (c) Microsoft Corporation.  All rights reserved.
// This source code is made available under the terms of the Microsoft Public License (MS-PL)

// Manager class records the various blocks so it can split them up
class Manager
{
	private struct Block {
		public String Name;
		public int Start, Length;
	}

	private List<Block> blocks = new List<Block>();
	private Block currentBlock;
	private Block footerBlock = new Block();
	private Block headerBlock = new Block();
	private ITextTemplatingEngineHost host;
	private ManagementStrategy strategy;
	private StringBuilder template;
	public String OutputPath { get; set; }

	public Manager(ITextTemplatingEngineHost host, StringBuilder template, bool commonHeader) {
		this.host = host;
		this.template = template;
		OutputPath = String.Empty;
		strategy = ManagementStrategy.Create(host);
	}

	public void StartBlock(String name) {
		currentBlock = new Block { Name = name, Start = template.Length };
	}

	public void StartFooter() {
		footerBlock.Start = template.Length;
	}

	public void EndFooter() {
		footerBlock.Length = template.Length - footerBlock.Start;
	}

	public void StartHeader() {
		headerBlock.Start = template.Length;
	}

	public void EndHeader() {
		headerBlock.Length = template.Length - headerBlock.Start;
	}	

	public void EndBlock() {
		currentBlock.Length = template.Length - currentBlock.Start;
		blocks.Add(currentBlock);
	}

	public void Process(bool split) {
		String header = template.ToString(headerBlock.Start, headerBlock.Length);
		String footer = template.ToString(footerBlock.Start, footerBlock.Length);
		blocks.Reverse();
		foreach(Block block in blocks) {
			String fileName = Path.Combine(OutputPath, block.Name);
			if (split) {
				String content = header + template.ToString(block.Start, block.Length) + footer;
				strategy.CreateFile(fileName, content);
				template.Remove(block.Start, block.Length);
			} else {
				strategy.DeleteFile(fileName);
			}
		}
	}
}

class ManagementStrategy
{
	internal static ManagementStrategy Create(ITextTemplatingEngineHost host) {
		return (host is IServiceProvider) ? new VSManagementStrategy(host) : new ManagementStrategy(host);
	}

	internal ManagementStrategy(ITextTemplatingEngineHost host) { }

	internal virtual void CreateFile(String fileName, String content) {
		File.WriteAllText(fileName, content);
	}

	internal virtual void DeleteFile(String fileName) {
		if (File.Exists(fileName))
			File.Delete(fileName);
	}
}

class VSManagementStrategy : ManagementStrategy
{
	private EnvDTE.ProjectItem templateProjectItem;

	internal VSManagementStrategy(ITextTemplatingEngineHost host) : base(host) {
		IServiceProvider hostServiceProvider = (IServiceProvider)host;
		if (hostServiceProvider == null)
			throw new ArgumentNullException("Could not obtain hostServiceProvider");

		EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
		if (dte == null)
			throw new ArgumentNullException("Could not obtain DTE from host");

		templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
	}

	internal override void CreateFile(String fileName, String content) {
		base.CreateFile(fileName, content);
		((EventHandler)delegate { templateProjectItem.ProjectItems.AddFromFile(fileName); }).BeginInvoke(null, null, null, null);
	}

	internal override void DeleteFile(String fileName) {
		((EventHandler)delegate { FindAndDeleteFile(fileName); }).BeginInvoke(null, null, null, null);
	}

	private void FindAndDeleteFile(String fileName) {
		foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems) {
			if (projectItem.get_FileNames(0) == fileName) {
				projectItem.Delete();
				return;
			}
		}
	}
}#>

[)amien

14 responses  

  1. Luis Rocha on January 22nd, 2009

    Damien,

    This is awesome!! Thanks for sharing this. I saw that the VSManagementStrategy class automatically adds the generated files to the project. Would all generated files show in the solution explorer as nested files of their t4 template?

    Thanks,
    Luis

  2. Damien Guard on January 22nd, 2009

    If it detects it’s running inside VS they all magically appear as nested files behind the template, if you turn it off they are removed again :)

    [)amien

  3. Pingback Dew Drop - January 23, 2009 | Alvin Ashcraft's Morning Dew on January 23rd, 2009

    [...] Multiple Outputs from T4 Made Easy (Damien Guard) [...]

  4. Vish on January 23rd, 2009

    Hi Damien,

    This is some great work. I had been searching the web for this info for a while now. You saved me a lot of work and frustration …

    Thank You,
    Vish

  5. George J. Capnias on January 23rd, 2009

    Damien, are you aware of the T4Toolbox project on CodePlex?

    Regards,
    George J.

  6. Damien Guard on January 23rd, 2009

    I was aware of it but hadn’t seen what they are up to lately, I have now, thanks.

    [)amien

  7. hannes on February 13th, 2009

    Hello Damien,

    thank You! Your Linq To SQL T4 Templates are great!

    Regards
    Hannes

  8. Mike on February 19th, 2009

    Damien

    Made Easy!!! I was struggling with some of the concepts necessary to the task until I found your post

    Thanks!
    Mike

  9. Johannes on February 24th, 2009

    Thanks Damien, great work!

    A small problem though is that my files are under TFS source control, and if I edit the template file it and its default output file are automatically checked out, but my additional files are not, and the file update crashes. Its easy enough to avoid by checking the files out first, but is it possible to automatically check out the other files as well before the Manager edits them?

    Johannes

  10. Mel Grubb on October 23rd, 2009

    Have you tried running this on VS2010b2 yet? I’m trying to work with a project of mine, and I’m not having much luck. I’m getting the following error:

    Compiling transformation: The type or namespace name ‘ITextTemplatingEngineHost’ could not be found (are you missing a using directive or an assembly reference?)

    Has something been moved, or made obsolete? Also, it would also appear that the behavior of the keyword may have changed. I have a particular template that must use reflection to accomplish its job, and it no longer finds the assemblies in the bin folder like it used to.

  11. Damien Guard on October 28th, 2009

    There is a problem with T4 in VS2010 whereby that interface was moved to Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll and so it can’t find it. You can add an assembly reference to it for now but I’ve been assured that it will be fixed for RTM:

    <#@ assembly name="C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll" #>

    (remove the (x86) bit on 32-bit machines).

    [)amien

  12. Valk on October 30th, 2009

    After looking around the internet I found where the interface is hiding.

    try: import namespace=”Microsoft.VisualStudio.TextTemplating.Interfaces”

  13. Mel Grubb on November 2nd, 2009

    When I look in the directory you mentioned, I don’t see a …TextTemplating.Interfaces.dll, only a …TextTemplating.Modeling.dll. Specifying the import as suggested by Valk seems to have solved the problem for me, though.

  14. Tim on December 1st, 2010

    I love it! We we’re previously using mygeneration (a free t3/4 editor). This handles generation so much better because its native to VS.
    Thanks a 1000000.

Leave your response

  1. (kept private)