Thursday, December 30, 2010

The Seahawks and the Playoffs

If the Seattle Seahawks win their football game against the Rams early next year, a team with a losing record will make the NFL playoffs!

If I were the King of Football (or any other sport), I’d make a rule that a team would have to win at least as many games as they lose to make the playoffs.

If a division winner had a losing record, that playoff position would go to an additional wild card team. The New York Giants and Tampa Bay (both have 9-6 records and would lose tie breakers to Green Bay) may be home watching the Seahawks. Under my scheme New Orleans could host a game.

In College Football, a team must have an even record to qualify for a bowl game. There was a big deal around here about the Washington Huskies having to win the Apple Cup in Pullman to get to 6-6 to qualify for a bowl.

Sunday, December 19, 2010

Generating a Nullable ADO.NET DataSet Wrapper

Part 4: The Template and putting it all together

So, I have my target code, my DataSet describer and some T4 template tools (including the ITextTemplatingEngineHost class I took from MSDN), but I haven’t generated any DataSet Wrapper code! It is Christmas time, but that’s still a lame excuse.

Running the Template

In Part 3, I created a class to pass parameters to a T4 Template and an overly simple template that echoed the parameters to the generated document. Here I’m using the same basic from the Test Console Application, but I am referencing a template that will generate the DataSet Wrapper and passing the appropriate parameters to generate the wrapper I want. This code use a DataSet in an assembly.
// Set up some parameters:
var parms = new TTCommucator.GetParams("setting.txt");
parms.CurrentParameters.Add("DsSource", "Assembly");
parms.CurrentParameters.Add("AssembyPath", @"C:\GeneratorProject\MyAssembly.dll");
parms.CurrentParameters.Add("DataSetName", "MyDS");
parms.CurrentParameters.Add("FileNameSpace", "MyAssembly.DataSets");
parms.CurrentParameters.Add("DsNameSpace", "NewProject.DataSetWrappers");
parms.Save();

// Set the Template File
string templateFileName = "DataSetX.tt";

// Set up host and engine
CustomCmdLineHost host = new CustomCmdLineHost();
Engine engine = new Engine();
host.TemplateFileValue = templateFileName;

//Read the text template.
string input = File.ReadAllText(templateFileName);

//Transform the text template.
string result = engine.ProcessTemplate(input, host);
// Save logic would be here…

Notes on the Template

In the template, I am using TTCommucator.GetParams to read the parameter I’ve written in code above; same as in Part 3. I use the DataSetDesciber.ReflectDataSet.DataSetDef from Part 2 to get the tables, columns, types, etc. from the target DataSet. The generated code will be different from the target code in Part 1. For example:
  • I’m using .NET type names (like System.Int32) instead of C# type names (like int). That’s what Type.ToString() returns and I see no reason to convert it just to look pretty.
  • I’m not using Nullable’s ? operator. No good reason, it just seems more consistent with the .NET types in code (I don’t see System.Int32? in code all that often).
The purpose of the target code is to provide a starting point for generation. The code will change as a result of what you learn in writing the generation and testing. In the Template I am generating C# code, but the template code is in VB.NET; it is easier for me to keep straight which is which.

The Template

<#@ template debug="true" hostspecific="true" language="VB" #>
<#@ output extension=".cs" #>
<#@ assembly name="C:\GeneratorProject\TTCommucator.dll" #>
<#@ assembly name="C:\GeneratorProject\DataSetDesciber.dll" #>
<#@ import namespace="DataSetDesciber.ReflectDataSet" #>
<#@ import namespace="TTCommucator" #>
<# 
    ' Load the Parameter object
    Dim parms As new GetParams(Host.ResolvePath("setting.txt"))
    parms.Load()

    ' Get the namespace
    Dim FileNameSpace As String = parms.CurrentParameters.Item("FileNameSpace")
    Dim DsNameSpace As String = parms.CurrentParameters.Item("DsNameSpace")

    ' a description of the DataSet to be wrapped
    Dim refx As DataSetDef = GetDsDescripter(parms)
#>
// Generated with DataSetWrapper Version 1
using System;
using System.Collections;
using System.Collections.Generic;
using <#=DsNameSpace #>;

namespace <#=FileNameSpace #>
{
    public class <#=refx.DataSetName #>X
    {
        private <#=refx.DataSetName #> _ds;
        public <#=refx.DataSetName #>X(<#=refx.DataSetName #> ds)
        {
            _ds = ds;
        }
<#For Each table As TableDef In refx.TableDefList #>
        public <#=table.TableName#>DataTableX <#=table.TableName#>
        {
            get { return new <#=table.TableName#>DataTableX(_ds.<#=table.TableName#>); }
            //set { _ds.<#=table.TableName#> = value; }
        }
<#Next#>
        public <#=refx.DataSetName #> DataSet
        {
            get { return _ds; }
        }
    }

<#For Each table As TableDef In refx.TableDefList #>
    public class <#=table.TableName#>DataTableX: IEnumerable<<#=table.TableName#>RowX>
    {
        private <#=refx.DataSetName #>.<#=table.TableName#>DataTable _theTable;
        public <#=table.TableName#>DataTableX(<#=refx.DataSetName #>.<#=table.TableName#>DataTable table)
        {
            _theTable = table;
        }

        public <#=refx.DataSetName #>.<#=table.TableName#>DataTable Table
        {
            get { return _theTable; }
        }

        #region IEnumerable<<#=table.TableName#>RowX> Members

        public IEnumerator<<#=table.TableName#>RowX> GetEnumerator()
        {
            //return new <#=table.TableName#>RowXEnum(_rows);
            return new <#=table.TableName#>TableEnum(_theTable);
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (IEnumerator)this;
        }

        #endregion
    }

    public class <#=table.TableName#>TableEnum : IEnumerator<<#=table.TableName#>RowX>
    {
        private <#=refx.DataSetName #>.<#=table.TableName#>DataTable _collection;
        private int curIndex;
        private <#=table.TableName#>RowX curBox;

        public <#=table.TableName#>TableEnum(<#=refx.DataSetName #>.<#=table.TableName#>DataTable collection)
        {
            _collection = collection;
            curIndex = -1;
            curBox = default(<#=table.TableName#>RowX);
        }

        public bool MoveNext()
        {
            //Avoids going beyond the end of the collection.
            if (++curIndex >= _collection.Count)
                return false;
            else
                // Set current <#=table.TableName#>RowX to next item in collection.
                curBox = new <#=table.TableName#>RowX((<#=refx.DataSetName #>.<#=table.TableName#>Row)_collection.Rows[curIndex]);
            return true;
        }

        public void Reset() { curIndex = -1; }

        public void Dispose() { }

        public <#=table.TableName#>RowX Current
        {
            get { return curBox; }
        }

        object IEnumerator.Current
        {
            get { return curBox; }
        }
    }

    public class <#=table.TableName#>RowX
    {
        <#=refx.DataSetName #>.<#=table.TableName#>Row _row;
        public <#=table.TableName#>RowX(<#=refx.DataSetName #>.<#=table.TableName#>Row row)
        {
            _row = row;
        }
        /// <summary>
        /// The wrapped DataRow
        /// </summary>
        public <#=refx.DataSetName #>.<#=table.TableName#>Row Row
        {
            get { return _row; }
        }
        #region Properties
        <#For Each column As ColumnDef In table.ColumnDefList#>
            <#If column.AllowDBNull Then#>


        public <#=GetNullableType(column.DataType.ToString())#> <#=column.ColumnName#>
        {
            get
            {
                if (_row.Is<#=column.ColumnName#>Null())
                    return null;
                else
                    return _row.<#=column.ColumnName#>;
            }
            set
            {
                if (<#=GetHasValueFunction(column.DataType.ToString())#>)
                    _row.Set<#=column.ColumnName#>Null();
                else
                    _row.<#=column.ColumnName#> =  
                        <#=GetNullableValueName(column.DataType.ToString())#>;
            }
        }
        <#Else#>

        public <#=column.DataType.ToString()#> <#=column.ColumnName#>
        {
            get { return _row.<#=column.ColumnName#>; }
            set { _row.<#=column.ColumnName#> = value; }
        }
        <#End If#>
    <#Next#>

        #endregion Properties
    }
<#Next#>
}
<#+ 
    ''' <summary>
    ''' Gets a DataSetDef bases on parms passed in
    ''' </summary>
    ''' <param name="parms">
    ''' The GetParms object containing parameters needed to create DataSetDef:<br />
    ''' AssembyPath: The path of the assembly containig the DataSet<br />
    ''' DataSetName: The fully qualified name of the dataset
    ''' </param>
    ''' <returns>The DataSetDef</returns>
    Function GetDsDescripter(parms As GetParams) AS DataSetDef
        Dim AssembyPath As String = parms.CurrentParameters.Item("AssembyPath")
        Dim DataSetName As String = parms.CurrentParameters.Item("DataSetName")
        Return DataSetReflector.GetDataSetDef(AssembyPath, DataSetName)
    End Function
    ''' <summary>
    ''' Gets the nullable type name for a given type name
    ''' </summary>
    ''' <param name="typeName">
    ''' The type name as a string (as returned from Type.ToString())</param>
    ''' <returns>The Nullable type name</returns>
    ''' <remarks>
    ''' Since strings are nullable and not compatible with Generic Nullable class, 
    ''' must be treated special 
    ''' </remarks>
    Function GetNullableType(typeName As String) As String
        Dim rVal as string
        Select Case typeName
        Case "System.String"
            rVal = typeName
        Case Else
            rVal = "Nullable<" & typeName & ">"
        End Select
        Return rVal
    End Function
    ''' <summary>
    ''' Gets the string used to check nullablity in a set property method
    ''' </summary>
    ''' <param name="typeName">
    ''' The type name as a string (as returned from Type.ToString())
    ''' </param>
    ''' <returns>
    ''' string of an expression that will return true if value is null
    ''' </returns>
    ''' <remarks>
    ''' Most nullable types have a HasValue property but not all
    ''' </remarks>
    Function GetHasValueFunction(typeName As String) As String
        Dim rVal as string
        Select Case typeName
        Case "System.String"
            rVal = "System.String.IsNullOrEmpty(value)"
        Case Else
            rVal = "!value.HasValue"
        End Select
        Return rVal
    End Function
    ''' <summary>
    ''' Gets the string used on the right side of set property method
    ''' </summary>
    ''' <param name="typeName">
    ''' The type name as a string (as returned from Type.ToString())
    ''' </param>
    ''' <returns>The Value property name</returns>
    ''' <remarks>
    ''' Most nullable types have a Value property but not all
    ''' </remarks>
    Function GetNullableValueName(typeName As String) As String
        Dim rVal as string
        Select Case typeName
        Case "System.String"
            rVal = "value"
        Case Else
            rVal = "value.Value"
        End Select
        Return rVal
    End Function
#>

Generating a Nullable ADO.NET DataSet Wrapper: The Series

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