Saturday, June 16, 2012

Tutorial (part 2)

In the first post of this tutorial, we created a very simple T4 text template and generated a textual output containing the string Hello World!. In this second part I will show how you can add behavior to your T4 text template by using C#.

We will start by using the T4 text template from part 1 of this tutorial.

<#@ template language="C#" #>
Hello World!

Additionally to saying “hello” to the world, we want to say hello to our friends Mary, Paul, and Peter. The simplest way to do this, would be just adding some new lines to the text template, but we would have to add a new line for each new friend. Therefore we need some logic inside the template that is able to process parameters passed to the T4 text template generator.


In the C# code create a new list containing the names of our friends and pass this list to the Generate method of the T4 text template generator.

public static void Generate()
{
IGenerator generator = new Generator();

var stream = new MemoryStream();
var textWriter = new StreamWriter( stream );

IList<string> friends = new List<string> {"Mary", "Peter", "Paul"};
generator.Generate(textWriter, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Part2\Part2.t4"), friends);

stream.Seek( 0, SeekOrigin.Begin );
var textReader = new StreamReader( stream );

var generatedText = textReader.ReadToEnd();
}

To read the parameters, the template needs a parameter directive for each parameter we pass to the T4 text template generator. Because we want to pass a list of names, the data type for the parameter is IList<string>. The generic IList interface is not contained in the System namespace, thus we need an additional import directive for the  System.Collections.Generic namespace. The import directive is the equivalent to the using statement in C# or the imports in VB.

<#@ template language="C#" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ parameter type="IList<string>" name="friends"#>
Hello World!

To use control logic in a template or to generate dynamic text output, you have to add a control block. A control block is a part of a T4 text template that contains code. The code can be either C# or VB. The language must be specified in the language attribute of the template directive. T4 knows three different types of control blocks: Standard Control Block, Expression Control Block, and Feature Control Block. A standard control block is delimited by the T4 symbols <# ... #> and can contain any valid C# or VB statements.

<#  foreach(var friend in friends)
{
Write ( "Hello " + friend + "!\r\n" );
}
#>

In the above code we use the generator’s Write method to create the textual output. If the text becomes more complex, it would be better to use text blocks for the static parts of the output text and expression control blocks for the dynamic part. An expression control block is delimited by the T4 symbols <#= ... #> and can contain C# or VB expressions.

<#  foreach(var friend in friends)
{
#>
Hello <#= friend #>!
<# }
#>

Finally we want to greet our friends in an alphabetical order. Therefore we add a feature control block containing a helper function that sorts out friends names.
A feature control block can contain properties or methods and is delimited by <#+ ... #>. Typically feature control blocks contain helper functions that can be used within a standard or expression control block. In our example, we will add a method to sort the names of our friends.

<#+ private IEnumerable<string> SortFriends(IEnumerable<string> friends)
{
return (from friend in friends
orderby friend ascending
select friend);
}
#>

The implementation of the SortFriends method uses LINQ so we must add a new import directive for the System.Linq namespace. Further, LINQ is provided by the System.Core .NET assembly which is not referenced by default. To use LINQ, you must also add an assembly directive.

<#@ template language="C#" #>
<#@ assembly name="System.Core.dll" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Linq" #>
<#@ parameter type="IList<string>" name="friends"#>

To use the SortFriends method, modify the foreach statement as described below.

<#    foreach(var friend in SortFriends(friends))

If you now run the generator with the modified T4 text template, you will get the following textual output:

Hello World!

Hello Mary!
Hello Paul!
Hello Peter!

Multiple parameters

The parameters parameter of the Generate method is marked with the params keyword, so you can pass a variable number of objects. You must pass these objects in the same sequence as they appear in parameter directives of the T4 text template.
Be aware that the parameter list is not type safe but you can create an extension method that contains exactly the parameters you need.
public static class GeneratorExtension
{
public static void Generate(this IGenerator generator, TextWriter output, string template, IList<string> friends)
{
generator.Generate( template, output, friends );
}
}

9 comments:

  1. Fine work. I am working on code generation for my application and came across your work. Thanks for putting this up.

    I have tried it and have been getting error messages when passing a class as a parameter. I can send you the code, but its basically passing a class of type MyApp.MyClass as a parameter and I get the error OMS.Ice.T4Generator.T4CompilerException : Error while building generator assembly.

    At the moment I am loking at codeplex project http://customtemplating.codeplex.com/ which has good error reporting and also handles the problem of Domain memory buildup. I think I will stick with that but do keep me up to date on what you are doing.

    Jon.Smith (at) selectiveanalytics.com

    ReplyDelete
    Replies
    1. Hi Jon,

      probably you forgot to add a reference to your assembly.

      IGenerator generator = new Generator();
      generator.Settings.ReferenceAssemblies.Add("AssemblyPath");

      The T4CompilerException contains detailed information. The GeneratedCode property contains the generated code and the Errors property contains a list of error messages received from the .NET compiler.

      Olaf

      Delete
    2. This comment has been removed by the author.

      Delete
    3. Hi Olaf,

      Thanks for that. Adding the line below after creating the generator fixed the problem.
      generator.Settings.ReferenceAssemblies.Add(Assembly.GetAssembly(typeof(MyClass)).FullName);

      Another question. Is there a problem with buildup of Assemblies used to generate templates? I read that this can be a problem. Have you come across this?

      Thnaks again for your advice.

      Jon Smith - Selective Analytics

      Delete
    4. Hi Jon,

      what exactly do you mean with "buildup of assemblies used to generate templates"?

      Delete
    5. Hi Olaf,

      While researching this and trying to build my own T4 Host I came across this link http://msdn.microsoft.com/en-us/library/bb126579.aspx which talks about how to create your own host. Under the public AppDomain ProvideTemplatingAppDomain(string content) method, which provides a new domain to run the Templating in, it talks about how this can grow. The comments are:

      //This could be changed to return the current appdomain, but new
      //assemblies are loaded into this AppDomain on a regular basis.
      //If the AppDomain lasts too long, it will grow indefintely,
      //which might be regarded as a leak.

      Also I saw in the codeplex project http://customtemplating.codeplex.com/ that Tim Cools has implemented a domain recycling process that throws away the domain after x uses (default 25).

      Now I haven't got far enough to see if this is a problem so I'm maybe not being very agile here but it does sound like a) domains take time to create so you don't want to create them every time (I see you have some sort of cache system which I assume is to dealt with this), and b) you don't want to hang onto a domain too long as it could grow too big. I expect to process about 200 to 500 output files coming from about 100 different templates so this could be an issue.

      I wondered if you had this problem or is it a non-problem nowadays with lots of RAM? Everyone says 'get it working and them performance tune' so maybe I should ignore it and press on.

      Thanks

      Jon Smith - Selective Analytics

      PS. Now you have solved the problems I had with OMS.Ice I plan to swap to your code.

      Delete
    6. Hi Jon,

      100 templates means 100 assemblies will be generated and loaded into the process. This isn't a problem at all. I did a test with 1000 templates and it worked very well. You don't need to compile and run the templates in an AppDomain.

      The OMS.Ice - T4Generator builds exactly one assembly for each template and keeps them loaded in the process. OMS.Ice creates a new instance of the generator class each time it is used and frees it after the text has been generated. However, if you really need to unload the assemblies, you are free to create your own AppDomain.

      Building hundreds of assemblies is very time consuming because the csc.exe (C# compiler) will be startet for each template. This is a general problem and not assigned to OMS.Ice only. You can use the TPL for better performance:

      var files = Directory.GetFiles( pathToT4, "*.t4" );
      Parallel.ForEach( files, file =>
      {
      var stream = new MemoryStream();
      var textWriter = new StreamWriter( stream );

      Generator.Generate( BuildPath( file ), textWriter, new object[]{} );
      } );

      In the next version, I will add a Compile() method, so a template can be precompiled in a background thread.


      Olaf

      Delete
    7. Hi Olaf,

      OK, you have put my mind at rest on the assemblies taking memory. Thanks. This does show that I should keep a reference to your generator and reuse that every time rather than creating a new one every time. I will plan that.

      I know generation of templates is time consuming and I am thinking about that. I might try moving to preprocessed templates, but that has some down sides. I think I will see how I progress and work out the best way once I understand the problem space better.

      Thanks for your help and input.

      Jon - Selective Analytics

      Delete
  2. Interesting and useful information that you have provided here on your post.Thanks.

    load bank hire & generator maintenance

    ReplyDelete