3

I'm trying to use T4 templates to make generating migrations for our system slightly easier. The one thing that I can't quite figure out (and this makes me wonder if I'm using T4 templates for the wrong thing) is how to copy the rendered output to a new file. I can manually create a file and copy the contents of the generated file, but that kind of goes against my whole "make things easier" ethos here.

Here's the template I have. Upon rendering, it would ideally get copied to "62-CreateWidgetsTable.cs" in the same directory. The goal is to have a file that I can now edit (I am generating a template, in other words, not generating the complete file.) If I could rename the generated file in VS (and then have the t4 generate a new template that would just sit there until someone came along and used it), that would be good enough.

  <#@ template debug="false" hostspecific="false" language="C#" #>
  <#@ output extension=".cs" #>
  <#
    var migrationNumber = "62";
    var migrationName = "CreateWidgetsTable";
  #>
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using Migrator.Framework;

  namespace WidgetsIntl.Console.Migrations
  {
    [Migration(<#= DateTime.UtcNow.ToString("yyyyMMddhhmmss") #>)]
    public class _<#= migrationNumber  #>_<#= migrationName #> : Migration
    {
      public override void Up()
      {

      }

      public override void Down()
      {

      }
    }
  }
TeaDrivenDev
  • 6,591
  • 33
  • 50
davidtbernal
  • 13,434
  • 9
  • 44
  • 60

3 Answers3

4

Alright, I figured out a couple ways to do this. The simplest way to do it (which I found only after doing it the way I'm about to show you) is here: t4 append output to existing file. The key information is GenerationEnvironment is a StringBuilder which contains the result of running the template, so you can just write that result to any old file you want!

The other way to do it is to use T4 Toolbox. Go download it!

Then, you can make a template that create a class that extends Template (defined by T4 toolbox) that overrides some default behavior:

<#@ template language="C#" hostspecific="True" debug="True" #>
<#@ include file="T4Toolbox.tt" #>
<#
    // make an instance of the class we define below, set some variables, and render it
    var tpl = new MyT4();
    tpl.MyVar = "Do those things you do";
    tpl.Render();
#>
<#+
public class MyT4 : Template 
{
    public MyVar = "some stuff";

    public override string TransformText()
    {
        Output.PreserveExistingFile = true; // tells T4 that you want to manually edit this file afterward (for scaffoling, which was my use case)
        Output.File = MyVar + ".cs"; // output will go in "some stuff.cs"

        /******************
        Template is defined here!
        *******************/
    #>
    public class <#=myVar.Replace(" ", "_") #> 
    { 
        public void Method()
        {
            return "Hi, I am <#= myvar #>";
        }
    }
    <#+
        /*************************
        now finishing up the TransformText() method
        *************************/

        return GenerationEnvironment.ToString();
    }
}
#>
Community
  • 1
  • 1
davidtbernal
  • 13,434
  • 9
  • 44
  • 60
3

In some Projects I use allready the FileManager class below. Its a custominated implementation based on this blog post: http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited

<#@ assembly name="System.Core"
#><#@ assembly name="System.Data.Linq"
#><#@ assembly name="EnvDTE"
#><#@ assembly name="System.Xml"
#><#@ assembly name="System.Xml.Linq"
#><#@ import namespace="System"
#><#@ import namespace="System.CodeDom"
#><#@ import namespace="System.CodeDom.Compiler"
#><#@ import namespace="System.Collections.Generic"
#><#@ import namespace="System.Data.Linq"
#><#@ import namespace="System.Data.Linq.Mapping"
#><#@ import namespace="System.IO"
#><#@ import namespace="System.Linq"
#><#@ import namespace="System.Reflection"
#><#@ import namespace="System.Text"
#><#@ import namespace="System.Xml.Linq"
#><#@ import namespace="Microsoft.VisualStudio.TextTemplating"
#><#+

// Manager class records the various blocks so it can split them up
protected abstract class FileManager {

    protected FileManager(ITextTemplatingEngineHost host, StringBuilder template)
    {
        this.host = host;
        this.template = template;
    }

    protected abstract void CreateFile(String fileName, String content);
    public abstract String GetCustomToolNamespace(String fileName);
    public abstract String DefaultProjectNamespace { get; }
    public abstract void Process();

    public static FileManager Create(ITextTemplatingEngineHost host, StringBuilder template) 
    {
        return new VSManager(host, template);
    }

    protected class Block
    {
        public String Name;
        public int Start, Length;
    }

    protected Block currentBlock;
    protected List<Block> files = new List<Block>();
    protected Block footer = new Block();
    protected Block header = new Block();
    protected ITextTemplatingEngineHost host;
    protected StringBuilder template;

    public void StartNewFile(String name) 
    {
        if (name == null)
            throw new ArgumentNullException("name");

        CurrentBlock = new Block { Name = name };
    }

    public void StartFooter() {
        CurrentBlock = footer;
    }

    public void StartHeader() {
        CurrentBlock = header;
    }

    public void EndBlock() {
        if (CurrentBlock == null)
            return;
        CurrentBlock.Length = template.Length - CurrentBlock.Start;
        if (CurrentBlock != header && CurrentBlock != footer)
            files.Add(CurrentBlock);

        currentBlock = null;
    }

    protected bool IsFileContentDifferent(String fileName, String newContent) 
    {
        return !(File.Exists(fileName) && File.ReadAllText(fileName) == newContent);
    }

    protected Block CurrentBlock 
    {
        get { return currentBlock; }
        set {
            if (CurrentBlock != null)
                EndBlock();
            if (value != null)
                value.Start = template.Length;
            currentBlock = value;
        }
    }

    // VS Manager
    private class VSManager: FileManager 
    {
        private EnvDTE.ProjectItem templateProjectItem;
        private EnvDTE.DTE dte;
        private List<string> generatedFileNames = new List<string>();

        public override String DefaultProjectNamespace 
        {
            get 
            {
                return templateProjectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString();
            }
        }

        public override String GetCustomToolNamespace(string fileName) 
        {
            return dte.Solution.FindProjectItem(fileName).Properties.Item("CustomToolNamespace").Value.ToString();
        }

        public override void Process() 
        {           
            EndBlock();
            String headerText = template.ToString(header.Start, header.Length);
            String footerText = template.ToString(footer.Start, footer.Length);

            Directory.SetCurrentDirectory(Path.GetDirectoryName(host.TemplateFile));

            files.Reverse();
            foreach(Block block in files) 
            {
                String fileName = Path.GetFullPath(block.Name);
                String content = headerText + template.ToString(block.Start, block.Length) + footerText;
                generatedFileNames.Add(fileName);
                CreateFile(fileName, content);
                template.Remove(block.Start, block.Length);
            }

            this.ProjectSync(generatedFileNames);
            this.files = new List<Block>();
            this.footer = new Block();
            this.header = new Block();
            this.generatedFileNames = new List<string>();
        }

        protected override void CreateFile(String fileName, String content)
        {
            if (IsFileContentDifferent(fileName, content)) 
            {
                CheckoutFileIfRequired(fileName);
                File.WriteAllText(fileName, content);
            }
        }

        internal VSManager(ITextTemplatingEngineHost host, StringBuilder template) : base(host, template) 
        {
            var hostServiceProvider = host as IServiceProvider;
            if (hostServiceProvider == null)
            {
                throw new ArgumentNullException("Could not obtain IServiceProvider");
            }

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

        private void ProjectSync(IEnumerable<string> keepFileNames) {
            var projectFiles = new Dictionary<string, EnvDTE.ProjectItem>();

            foreach (string keepFileName in keepFileNames)
            {
                var item = this.dte.Solution.FindProjectItem(keepFileName);
                if (item != null)
                {
                    projectFiles.Add(keepFileName, item);
                }
            }

            // Remove unused items from the project 
            /* foreach(var pair in projectFiles) // NEW
            {
                if (keepFileNames.Contains(pair.Key))
                {
                    pair.Value.Delete();
                }
            } */

            // Add missing files to the project
            foreach(string fileName in keepFileNames)
            {
                if (!projectFiles.ContainsKey(fileName))
                {       
                    EnvDTE.Project targetProj = null;
                    foreach (EnvDTE.Project proj in this.dte.Solution.Projects)
                    {
                        if (string.IsNullOrEmpty(proj.FullName))
                        {
                            continue;
                        }

                        if (fileName.Contains(Path.GetDirectoryName(proj.FullName) + @"\"))
                        {
                            targetProj = proj;
                            break;
                        }
                    }       

                    var targetDir = NavigateTo(targetProj, fileName);       
                    if (targetDir == null)
                    {
                        targetProj.ProjectItems.AddFromFile(fileName);
                        continue;
                    }

                    targetDir.ProjectItems.AddFromFile(fileName);
                }
            }
        }

        private void CheckoutFileIfRequired(String fileName) 
        {
            var sc = dte.SourceControl;
            if (sc != null && sc.IsItemUnderSCC(fileName) && !sc.IsItemCheckedOut(fileName))
            {
                dte.SourceControl.CheckOutItem(fileName);
            }
        }

        public EnvDTE.ProjectItem NavigateTo(EnvDTE.Project project, string path)
        {
            if (string.IsNullOrEmpty(project.FullName))
            {
                return null;
            }

            var projBase = Path.GetDirectoryName(project.FullName);
            var fileBase = Path.GetDirectoryName(path);
            var naviBase = fileBase.Replace(projBase + @"\", "");

            if (string.IsNullOrEmpty(fileBase.Replace(projBase, "")))
            {
                return null;
            }

            var naviPoints = naviBase.Split('\\');
            EnvDTE.ProjectItem item = null;
            EnvDTE.ProjectItems items = project.ProjectItems;

            foreach (var folder in naviPoints)
            {
                item = items.Item(folder);
                items = item.ProjectItems;
            }

            return item;
        }
    }
} #>
TeaDrivenDev
  • 6,591
  • 33
  • 50
benwasd
  • 1,342
  • 10
  • 18
-1

The easiest way I have found to do this without plugins is to right-click your target project and go to Add -> Existing Item and select your generated file. This makes a copy of the file for you at the root of the project, which you can then move as needed. This allows you to easily transfer generated files to projects besides the one that they were generated in.

I haven't tested what happens when the .tt file is itself in the root of a project, but this definitely works so long as the .tt is in a subfolder (which is probably good practice anyhow).

ketura
  • 399
  • 1
  • 6
  • 11