Luke 的个人资料lzcd照片日志列表更多 工具 帮助

日志


6月1日

The templating engine that no talks about… at least not in front of the children

Here’s a little experiment for you to try in Visual Studio 2008:

  • Create a project – it doesn’t matter what sort but I’m using C# console app for this example
  • Add a new text file and give it the extension of .tt
  • Add the following lines to it:

<#@ template debug="true" #>

<# WriteLine(“Hello “); #>World

Now if you save the file you should note that there’s a little “code behind” file hiding below the .tt file in much the same fashion as a WinForms or an ASP.Net control.

And if we open it up we can see that it the result of the .tt file being executed as if it was a template:

Hello World

So viola! We have a templating engine built right into Visual Studio and there you were just about to spend big dollars on a third party solution. Don’t you feel silly now?

Well you shouldn’t feel quite so bad… yet… as the templating engine, T4 (as it’s officially known by Microsoft as) is all very nice but it’s not quite what I’m usually looking for in a templating engine. Quite often in such scenarios, what I want to do is grab some templates, grab some data and mix them all together en masse using a funky little console app to generate all sorts of weird and wonderful files for me. T4 just looks like it’s dealing with one file at a time and, even more annoyingly, I seem to need to be in Visual Studio to use it. This just won’t do.

So how we do fix this?

It just so happens that we can host the T4 scripting engine ourselves and do all sorts of weird and wonderful things with it. The “catch” is that if you do a search through the MSDN doco or related blogs on the T4 engine you’ll come across a boat load of API stuff that can all look rather intimidating. The good news is that we’re going to be producing a working host with little to no effort at all.

(Cue fanfare)

Presenting: Luke’s ultra quick starter guide to producing your own template crunching utility!

In this example I’m going to be constructing a console app that takes a template file, a set of command line arguments and combines them together to produce a new file. It’s not exactly the most spectacular bit of technology around but it should hopefully get you on your way enough to produce you very own ultimate templating processing machine.

Sooo…. the steps are as follows:

  • Create a console app project
  • Track down a copy of the Microsoft.VisualStudio.TextTemplating.dll file and add a reference to it. (The MSDN docs will tell you that you can get the file as part of the Visual Studio SDK… but Visual Studio actually secretly places a copy in the GAC which you can copy to you project folder manually if you’re feeling really sneaky)
  • Add the following code your Program.cs file:

static void Main(string[] args)
        {
            if (args.Length == 0 || !File.Exists(args[0]))
            {
                Console.WriteLine("Usage:");
                Console.WriteLine("CodeGen script [field0] [field1]...");
                return;
            }

            var commandLineArguments = new List<string>(args);
            var templateFilePath = commandLineArguments[0];
            var host = CreateHost(templateFilePath, commandLineArguments.Skip(1).ToList());
            var template = File.ReadAllText(templateFilePath);
            Engine engine = new Engine();
            var output = engine.ProcessTemplate(template, host);
            if (host.Errors.HasErrors)
            {
                foreach (var error in host.Errors)
                {
                    Console.WriteLine(error);
                }
                return;
            }

            var outputFilePath = Path.Combine(Path.GetDirectoryName(templateFilePath), Path.GetFileNameWithoutExtension(templateFilePath));
            File.WriteAllText(outputFilePath, output, host.FileEncoding);
            Console.WriteLine(outputFilePath + " Generated");
        }

        private static Host CreateHost(string templateFilePath, List<string> commandLineArguments)
        {
            var host = new Host();
            host.TemplateFile = templateFilePath;
            host.FileExtension = "";
            host.FileEncoding = Encoding.UTF8;
            host.StandardAssemblyReferences = new List<string>() { typeof(System.Uri).Assembly.Location };
            host.StandardImports = new List<string> { "System" };
            host.CommandLineArguments = commandLineArguments;
            return host;
        }

Most of this is quick and nasty throw away code (and not something I usually write… or at least admit to) so you can ignore most of it.

The valuable bit that is worth noting is the code that creates an instance of T4’s template Engine class, feeds it a template file along with a mysterious thing called a host and magically churns out a brand new file from it.

I’d suggest thinking of the Host as the middle man between your utility and the T4 templating engine… which brings us nicely to the next step which is…

  • Create a Host class and paste the following code into it:

[Serializable()]
  public class Host : ITextTemplatingEngineHost
  {
      public string TemplateFile { get; set; }
      public string FileExtension { get; set; }
      public Encoding FileEncoding { get; set; }
      public CompilerErrorCollection Errors { get; set; }
      public IList<string> StandardAssemblyReferences { get; set; }
      public IList<string> StandardImports { get; set; }

      public bool LoadIncludeText(string requestFileName, out string content, out string location)
      {
          content = string.Empty;
          location = string.Empty;

          if (!File.Exists(requestFileName))
          {
              return false;
          }

          content = File.ReadAllText(requestFileName);
          return true;
      }

      public object GetHostOption(string optionName)
      {
          switch (optionName)
          {
              case "CacheAssemblies":
                  return true;
              default:
                  return false;
          }
      }

      public string ResolveAssemblyReference(string assemblyReference)
      {
          if (File.Exists(assemblyReference))
          {
              return assemblyReference;
          }

          var candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
          if (File.Exists(candidate))
          {
              return candidate;
          }

          return string.Empty;
      }

      public List<string> CommandLineArguments { get; set; }

      public Type ResolveDirectiveProcessor(string processorName)
      {
          if (string.Equals(processorName, "CodeGen", StringComparison.InvariantCultureIgnoreCase))
          {
              CustomDirectiveProcessor.CommandLineArguments = CommandLineArguments;
              return typeof(CustomDirectiveProcessor);
          }
          throw new Exception("Directive processor for " + processorName + " not found");
      }

      public string ResolvePath(string path)
      {
          if (string.IsNullOrEmpty(path))
          {
              throw new ArgumentNullException("path", "Path cannot be null");
          }

          if (File.Exists(path))
          {
              return path;
          }

          var candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), path);
          if (File.Exists(candidate))
          {
              return candidate;
          }

          return path;
      }

      public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
      {
          if (string.IsNullOrEmpty(directiveId))
          {
              throw new ArgumentNullException("directiveId", "Directive cannot be null");
          }
          if (string.IsNullOrEmpty(processorName))
          {
              throw new ArgumentNullException("processorName", "Processor cannot be null");
          }
          if (string.IsNullOrEmpty(parameterName))
          {
              throw new ArgumentNullException("parameterName", "Parameter cannot be null");
          }

          return string.Empty;
      }

      public void SetFileExtension(string extension)
      {
          FileExtension = extension;
      }

      public void LogErrors(CompilerErrorCollection errors)
      {
          Errors = errors;
      }

      public AppDomain ProvideTemplatingAppDomain(string content)
      {
          return AppDomain.CreateDomain("Generation App Domain");
      }

      public void SetOutputEncoding(Encoding encoding, bool fromOutputDirective)
      {
          FileEncoding = encoding;
      }

  }

So now we have templates and a way to generate them. The thing that’s missing is a way to feed information from the outside world into the templates and the way we do this via Directive Processors. I’ll leave the fun of reading up about the specifics of these little beasties on MSDN as an exercise for the reader but here’s one I created earlier:

  • Create a new code file named CustomDirectiveProcessor and paste the following code into it:

public class CustomDirectiveProcessor : DirectiveProcessor
   {
       private const string includeArgumentsKeyword = "includeArguments";
       public override bool IsDirectiveSupported(string directiveName)
       {
           if (string.Equals(directiveName, includeArgumentsKeyword, StringComparison.InvariantCultureIgnoreCase))
           {
               return true;
           }

         return false;
       }

       public CodeDomProvider Provider { get; set; }
       public string TemplateContents { get; set; }
       public CompilerErrorCollection Errors { get; set; }
       private StringBuilder codeBuffer;

       public override void StartProcessingRun(CodeDomProvider languageProvider, string templateContents, CompilerErrorCollection errors)
       {
           Provider = languageProvider;
           TemplateContents = templateContents;
           Errors = errors;
           codeBuffer = new StringBuilder();
       }

       public static List<string> CommandLineArguments = new List<string>();

       public override void ProcessDirective(string directiveName, IDictionary<string, string> arguments)
       {
           var options = new CodeGeneratorOptions()
           {
               BlankLinesBetweenMembers = true,
               IndentString = "    ",
               VerbatimOrder = true
           };

           if (string.Equals(directiveName, includeArgumentsKeyword, StringComparison.InvariantCultureIgnoreCase))
           {
               GenerateCommandLineArgumentProperties(options);
           }

       }

       private void GenerateCommandLineArgumentProperties(CodeGeneratorOptions options)
       {
           for (int argumentIndex = 0; argumentIndex < CommandLineArguments.Count; argumentIndex++)
           {
               var property = new CodeMemberProperty()
               {
                   Name = "Argument" + argumentIndex,
                   Type = new CodeTypeReference(typeof(string)),
                   Attributes = MemberAttributes.Public,
                   HasGet = true,
                   HasSet = false
               };

               property.GetStatements.Add(new CodeMethodReturnStatement(new CodePrimitiveExpression(CommandLineArguments[argumentIndex])));

               using (StringWriter writer = new StringWriter(codeBuffer, CultureInfo.InvariantCulture))
               {
                   Provider.GenerateCodeFromMember(property, writer, options);
               }
           }
       }

       public override void FinishProcessingRun()
       {
           Provider = null;
       }

       public override string GetClassCodeForProcessingRun()
       {
           return codeBuffer.ToString();
       }

       public override string[] GetImportsForProcessingRun()
       {
           var thisNamespaceElements = this.GetType().ToString().Split('.');
           var thisNamespace = string.Join(".", thisNamespaceElements.Take(thisNamespaceElements.Count() - 1).ToArray());
           return new string[]
           {
              thisNamespace
           };
       }

       public override string GetPreInitializationCodeForProcessingRun()
       {
           return string.Empty;
       }

       public override string GetPostInitializationCodeForProcessingRun()
       {
           return string.Empty;
       }

       public override string[] GetReferencesForProcessingRun()
       {
           return new string[]
           {
               this.GetType().Assembly.Location
           };
       }
   }

So what does this directive processor do exactly? Well I’m glad you asked. :)

It allows us to extend the templating engine with our own functionality. In this case, we’re going to make it so that when some adds the following bit of code to their templates, we’re going to magically read in the command line arguments and supply them as properties:

So the only thing left to is to try it out on a template…

  • Create a textfile somewhere on your machine with the extension of .cs.txt (e.g. HelloWorld.cs.txt) and paste the following code into it:

<#@ template debug="true" #>

<#@ includeArguments Processor="CodeGen" #>

<# WriteLine(Argument1 + " contains " + Argument0); #>

  • Run the utility with the path of the textfile as the first argument and some random words as the second and third command line arguments (e.g. c:\temp\helloworld.cs.txt France Paris )

Hopefully you should see a brand new cs file in the folder your textfile resides in with some content (not entirely useful content I’ll admit… but template driven content none the less)

If you go back and examine the mysterious entity that is the CustomDirectiveProcessor class a little more thoroughly you may notice a few odd looking things:

  • I pass the command line arguments in via static property. Yep. As it’s the templating engine that creates the instances of processors as it needs them, the only way to “communicate” between our application code and the processors is via statics. (Actually there’s quite a few ways… but this was the simplest I could think of at the time and it required the least amount of architectural magic to get it working.)
  • We’re creating the properties using the CodeDom. Yep. The way the DirectiveProcessors influence the templates is via adding code dynamically. If you’re looking to do something slightly more heavyweight… or you just dislike generating code via the CodeDom intensely, there are less ugly ways to do so… but require a whole lot more time and effort than this demo warranted.

So now you know how to harness the inbuilt goodness that is the T4 templating engine for your evil purposes.

Oh… and before I forget: One last little party piece…

  • Add the following line to the end of your template text file:

<# System.Diagnostics.Debugger.Break(); #>

..and rerun your console app.

If you’ve got Visual Studio kicking around you should now be looking at one fully fledged breakpoint. Yep you can break and step through your templates just like any other Visual Studio code. It even supports variable inspections. Pretty cool, no?

Now all that’s left is to go forth and template yourself silly.

Enjoy.