Part 3: Detour hacking parameters for T4 Templates (VS 2008)
I decided to use T4 Templates instead of my home grown code generation techniques. I didn’t want to spend several pages on my way; besides, I probably need to get with the times and play with the same toys as the cool kids.
This post is to describe a punt. I couldn’t find a way to pass parameters to a VS 2008 .tt file. I know that VS 2010 has the parameter directive, but I don’t have VS 2010, so thus the hack below.
TTCommucator.GetParams
Since I can reference .NET assemblies, I am storing my parameters as a Dictionary to a file outside of the template and then reading it in from inside the template.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace TTCommucator
{
/// <summary>
/// Used to pass parameters to a T4 Template
/// </summary>
public class GetParams
{
private const char SEPERATOR_CHAR = '|';
private string _fileName;
/// <summary>
/// The Dictionary containing the Parameters
/// </summary>
public Dictionary<string, string> CurrentParameters { get; set; }
/// <summary>
/// Creates a GetParmas with a Filename
/// </summary>
/// <param name="fileName">The name of the file to store the parameters</param>
/// <remarks>Both sides of the transaction must agree on file name</remarks>
public GetParams(string fileName)
{
CurrentParameters = new Dictionary<string, string>();
this._fileName = fileName;
}
/// <summary>
/// Loads the data from the file into the Dictionary CurrentParameters
/// </summary>
public void Load()
{
CurrentParameters = new Dictionary<string, string>();
string line;
using (TextReader tr = new StreamReader(_fileName))
{
while ((line = tr.ReadLine()) != null)
{
string[] parts = line.Split(new char[] { SEPERATOR_CHAR }, 2);
if (parts.Count() > 1)
CurrentParameters.Add(parts[0], parts[1]);
}
tr.Close();
}
}
/// <summary>
/// Saves the data in the Dictionary CurrentParameters to the file
/// </summary>
public void Save()
{
using (TextWriter tw = new StreamWriter(_fileName))
{
foreach (var item in CurrentParameters)
{
tw.WriteLine(item.Key + SEPERATOR_CHAR + item.Value);
}
tw.Close();
}
}
}
}
Test Template
I created a simple template to demonstrate that I can receive parameters using my GetParams class. Note that I use VB.NET for code in my templates. Since I usually generate C# code, I find it easier to read templates when the template code isn’t C#, besides “<#}#>” is downright nasty looking.
<#@ template debug="true" hostspecific="true" language="VB" #>
<#@ assembly name="C:\Projects\TTCommucator\TTCommucator.dll" #>
<#@ import namespace="TTCommucator" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".txt" #>
<#
' Load the Parameters here:
Dim parms As new GetParams(Host.ResolvePath("setting.txt"))
parms.Load()
#>
Values in parms:
<#For Each item As KeyValuePair(Of String, String) In parms.CurrentSettings#>
<#=item.Key#> = <#=item.Value#>
<#Next#>
Test Console Application
Here is a simple program to test with. It doesn’t do anything more than necessary. NOTE: You will need to install Visual Studio 2008 SDK v1.1 to run this code:
using System;
using System.IO;
using Microsoft.VisualStudio.TextTemplating;
using TTCommucator;
namespace TTCommunicatorConsole
{
class Program
{
static void Main(string[] args)
{
// Set up some parameters:
var parms = new GetParams("setting.txt");
parms.CurrentParameters.Add("Greeting", "Hello World!");
parms.CurrentParameters.Add("Run Time", DateTime.Now.ToString());
parms.Save();
string templateFileName = "SimpleTemplate.tt";
// Set up host and engine
CustomCmdLineHost host = new CustomCmdLineHost();
Engine engine = new Engine();
host.SetTemplateFile(templateFileName);
//Read the text template.
string input = File.ReadAllText(templateFileName);
//Transform the text template.
string output = engine.ProcessTemplate(input, host);
// Show your work
Console.WriteLine(output);
// Wait for me
Console.ReadKey();
}
}
}
Host Class
I lifted this class from MSDN and removed the comments to save space. You can learn more about it by going here. NOTE: The MSDN code doesn't include the Serializable attribute, but I needed to add it to avoid errors. You will need to include a reference to Microsoft.VisualStudio.TextTemplating.
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.VisualStudio.TextTemplating;
namespace TTCommucator
{
[Serializable]
public class CustomCmdLineHost : ITextTemplatingEngineHost
{
internal string _templateFile;
public string TemplateFile
{
get { return _templateFile; }
}
private string _fileExtension = ".txt";
public string FileExtension
{
get { return _fileExtension; }
}
private Encoding _fileEncoding = Encoding.UTF8;
public Encoding FileEncoding
{
get { return _fileEncoding; }
}
private CompilerErrorCollection _errors;
public CompilerErrorCollection Errors
{
get { return _errors; }
}
public IList StandardAssemblyReferences
{
get { return new string[] { typeof(System.Uri).Assembly.Location }; }
}
public IList StandardImports
{
get { return new string[] { "System" }; }
}
public bool LoadIncludeText(string requestFileName, out string content,
out string location)
{
content = string.Empty;
location = string.Empty;
if (File.Exists(requestFileName))
{
content = File.ReadAllText(requestFileName);
return true;
}
else
return false;
}
public object GetHostOption(string optionName)
{
object returnObject;
switch (optionName)
{
case "CacheAssemblies":
returnObject = true;
break;
default:
returnObject = null;
break;
}
return returnObject;
}
public string ResolveAssemblyReference(string assemblyReference)
{
if (File.Exists(assemblyReference))
return assemblyReference;
string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile),
assemblyReference);
if (File.Exists(candidate))
return candidate;
return string.Empty;
}
public Type ResolveDirectiveProcessor(string processorName)
{
if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase)
== 0)
{
}
throw new Exception("Directive Processor not found");
}
public string ResolvePath(string fileName)
{
if (fileName == null)
throw new ArgumentNullException("the file name cannot be null");
if (File.Exists(fileName))
return fileName;
string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile),
fileName);
if (File.Exists(candidate))
return candidate;
return fileName;
}
public string ResolveParameterValue(string directiveId, string processorName,
string parameterName)
{
if (directiveId == null)
throw new ArgumentNullException("the directiveId cannot be null");
if (processorName == null)
throw new ArgumentNullException("the processorName cannot be null");
if (parameterName == null)
throw new ArgumentNullException("the parameterName cannot be null");
return string.Empty;
}
public void SetFileExtension(string extension)
{
_fileExtension = extension;
}
public void SetTemplateFile(string templateFile)
{
_templateFile = templateFile;
}
public void SetOutputEncoding(Encoding encoding,
bool fromOutputDirective)
{
_fileEncoding = encoding;
}
public void LogErrors(CompilerErrorCollection errors)
{
_errors = errors;
}
public AppDomain ProvideTemplatingAppDomain(string content)
{
return AppDomain.CreateDomain("Generation App Domain");
}
}
}
Generating a Nullable ADO.NET DataSet Wrapper: The Series