Understanding T4: <#@ parameter #> directive


Visual Studio 2010 introduced a new T4 directive called parameter. This directive provides a standard method for defining input parameters in text templates as well as passing parameter values at run time.

Syntax

<#@ parameter name="" type="" #>
name

Specifies name of the parameter and must be a valid C# or Visual Basic identifier.

type

Specifies type of the parameter and must include the complete namespace, such as System.String or System.Int32. A custom type can be used as well, as long as the assembly it is defined in is referenced with an assembly directive in the template.

Usage

During template transformation, parameters are available in template code as strongly-typed, read-only properties.

MyTemplate.tt
<#@ template language="C#" #>
<#@ parameter name="MyParameter" type="System.String" #>
Parameter in expression block: <#= MyParameter #>
Parameter in statement block: <# Write(MyParameter) #>

Parameter values can be supplied via remoting CallContext or the template transformation Session. Typically, this would be done in a Visual Studio extension that hosts its own instance of T4 engine or relies on the standard ITextTemplating service. While Visual Studio extensions are a fascinating subject, its discussion is outside of scope of this article. Instead, we will illustrate the technique of passing parameter values using template code itself.

Remoting CallContext

CallContext allows you to define global variables whose scope is limited to a single thread, also known as thread-local variables. While on the surface the CallContext is similar to Thread Local Storage in Windows, when a remoting call is made, whether across the network or simply from one AppDomain to another, the LogicalCallContext travels with the call. In other words, CallContext allows you to define thread-local variables that will work even with remote calls.

In Visual Studio, T4 templates are normally executed in a separate AppDomain, which is periodically unloaded to free memory. Therefore, remoting CallContext is a great way to specify template parameter values. The following example illustrates this can be done for the template shown above.

CallContextTemplate.tt
<#@ template language="C#" hostspecific="true" #>
<#@ output extension=".txt" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Runtime.Remoting.Messaging" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#
  string templateFile = this.Host.ResolvePath("MyTemplate.tt");
  string templateContent = File.ReadAllText(templateFile);

  CallContext.LogicalSetData("MyParameter", "CallContextValue");

  Engine engine = new Engine();
  string generatedContent = engine.ProcessTemplate(templateContent, this.Host);

  CallContext.FreeNamedDataSlot("MyParameter");

  this.Write(generatedContent);
#>
Template Transformation Session

In Visual Studio 2010, T4 also allows you to access template transformation Session similar to the Session in ASP.NET. Template transformation session allows you to define variables global for a particular template. Session variables can be passed from the transformation host to the template and can be used to define template parameter values. Here is how this can be done for the template shown above.

SessionTemplate.tt
<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#
  string templateFile = this.Host.ResolvePath("MyTemplate.tt");
  string templateContent = File.ReadAllText(templateFile);

  TextTemplatingSession session = new TextTemplatingSession();
  session["MyParameter"] = "SessionValue";

  var sessionHost = (ITextTemplatingSessionHost) this.Host;
  sessionHost.Session = session;

  Engine engine = new Engine();
  string generatedContent = engine.ProcessTemplate(templateContent, this.Host);

  this.Write(generatedContent);
#>

Under the Hood

T4 engine generates a private, read-only property for each parameter directive in your template. In particular, here is how the template shown above is compiled by T4.

public partial class MyTemplate : TextTransformation
{
    private string _MyParameterField;
    private string MyParameter
    {
        get { return this._MyParameterField; }
    }

    public override string TransformText()
    {
        this.GenerationEnvironment = null;
        this.Write("Parameter in expression block: ");
        this.Write(this.ToStringHelper.ToStringWithCulture(MyParameter));

        this.Write("\r\nParameter in statement block: ");
        Write(MyParameter);

        return this.GenerationEnvironment.ToString();
    }

    // …
}

Notice that MyParameter property is backed by a private field. The field is initialized by code in the Initialize method, invoked by T4 before the TransformText method which performs the actual code generation.

public partial class MyTemplate : TextTransformation
{
  // …

  public override void Initialize()
  {
    base.Initialize();
    if (this.Errors.HasErrors == false)
    {
      bool MyParameterValueAcquired = false;
      if (this.Session.ContainsKey("MyParameter"))
      {
        if (typeof(string).IsAssignableFrom(this.Session["MyParameter"].GetType()) == false)
        {
          this.Error("The type \’System.String\’ of the parameter \’MyParameter\’ did not match the type of" +
              " the data passed to the template.");
        }
        else
        {
          this._MyParameterField = (string)this.Session["MyParameter"];
          MyParameterValueAcquired = true;
        }
      }

      if (MyParameterValueAcquired == false)
      {
        object data = CallContext.LogicalGetData("MyParameter");
        if (data != null)
        {
          if (typeof(string).IsAssignableFrom(data.GetType()) == false)
          {
            this.Error("The type \’System.String\’ of the parameter \’MyParameter\’ did not match the type of" +
                " the data passed to the template.");
          }
          else
          {
            this._MyParameterField = (string)data;
          }
        }
      }
    }
  }

  // …
}

As you can see, the template will first try to retrieve the parameter value from its Session, and only if it doesn’t exist there, it will try the remoting CallContext.

Closing Thoughts

Parameter directive closes an important gap in the integration story of T4. In the past, most notably in ASP.NET MVC, this gap lead the tool developers using T4 down the wrong path of creating a custom ITextTemplatingEngineHost just to be able to pass parameters from the tool to the template. Although the remoting CallContext was available before and template parameters are just a thin wrapper around it, this new mechanism will be difficult to miss now that it is a part of the template syntax. Unfortunately, this may have come too late for MVC, which is bound by backward compatibility chains to the old custom host, making it impossible to use any but the limited, MVC-specific templates with this tool.

References


Write a Comment

Take a moment to comment and tell us what you think. Some basic HTML is allowed for formatting.

Reader Comments

[…] Understanding T4: <#@ parameter #> directive (Oleg Sych) […]

[…] Understanding T4: <#@ parameter #> directive - Oleg Sych takes a look at the new Parameter directive introduced to the T4 template language in VS2010 which makes it much easier to pass parameter into your templates for use within the template […]

How to use parameter directive with a preprocessed template?
I tried to build a dictionary and assign it to the Session property of the MyPreprocessedTemplate instance, but it doesn’t work. When debugging, it seems fine, but at one point Visual decides to parse the template and pretends that Param is null:

MyPreprocessedTemplate template = new MyPreprocessedTemplate();
IDictionary session = new Dictionary();
session[”Param”] = new MyObject();
template.Session = session;
Console.Write(template.TransformText());

li,

For pre-processed templates, you need to call the Initialize method explicitly. This is where the parameter values are extracted from the session dictionary, but this method is only invoked automatically during end-to-end template transformation.

Oleg