Thursday, December 09, 2010

Generating a Nullable ADO.NET DataSet Wrapper

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

No comments: