@PCSWebLabs I.T. for the I.T. Community

6Jun/090

Code Generation Part II – Creating a Code Generator With C#, XML and XSLT


Overview

This article is the second in a series of Code Generation articles that will demonstrate the basic concepts of using code in combination with freely available tools to generate code and eliminate hours of tedious and repetitive work. This article builds on the previous article in both knowledge and code and is recommended reading prior to moving forward. In this article we will examine key concepts in generating code from XML metadata with XML related objects from the System.Xml.Xsl assembly of the Microsoft .Net Framework. This article will deal with the following and build upon:

  • Specifying new elements in the App.config file, wrapping the keys in the LibraryConstants class, and new properties to the CoreBase class that will expose values set in the App.config file
  • Add support enumerations and classes to the CodeGenerator.Core assembly
  • Creating the CodeBuilder class that will derive from MetadataGenerator in order to create a single point of execution for code generation purposes
  • Create basic templates for the generation of stored procedures based on our database schema
  • Modifying the CodeGenerator application to make use of the finished CodeGenerator.Core assembly

Review

At this stage in the Code Generation project we have a basic console application that is able to extract SQL Server database metadata from the code generation target and display this data in XML format to the output screen. The application is able to do this by way of the CodeGenerator.Core assembly. It is this assembly that we will continue to expand further and give the assembly the ability to not just retrieve required metadata, but use this metadata for code generation purposes.

Configuration Options

We begin this section with the App.config file in the console application. We must edit this file in order to add information about XSL template file paths as well as generator output paths. Once the App.config has been fully modified, we will add the configuration keys to the LibraryConstants class and further modify the CoreBase class to expose these configuration elements as properties to all derived classes. Before we do so, let's first modify the console application project and add stub files (empty files) for the XSL templates. We will code these templates last as doing so now will become a distraction from our main goal which is to build a code generator. Let's begin by adding a folder to the console application project and naming it "Templates". Once done, add the following stub XSL files and name them as shown here:

  • Templates
    • DeleteProcedure.xsl
    • InsertProcedure.xsl
    • SearchProcedure.xsl
    • SelectProcedure.xsl
    • UpdateProcedure.xsl

We are now ready to modify the App.config file and add template path information. To do so, load the configuration file in Visual Studio and add the following elements to the "appSettings" section:

<!-- OUTPUT OPTIONS -->
<add key="OutputRoot" value="C:\Code Generator Output" />
		
<!-- STORED PROCEDURE TEMPLATES -->
<add key="DeleteProcedureTemplatePath" value="[PATH TO FILE]\DeleteProcedure.xsl" />
<add key="InsertProcedureTemplatePath" value="[PATH TO FILE]\InsertProcedure.xsl" />
<add key="SearchProcedureTemplatePath" value="[PATH TO FILE]\SearchProcedure.xsl" />
<add key="SelectProcedureTemplatePath" value="[PATH TO FILE]\SelectProcedure.xsl" />
<add key="UpdateProcedureTemplatePath" value="[PATH TO FILE]\UpdateProcedure.xsl" />

Next, we need to modify the path to the XSL templates in the configuration elements. We do this by deleting the data in the "value" attribute of the element and from the solutions explorer, drag and drop the XSL template file into the value attribute. The result of this operation will be the inclusion of the file path directly into the value attribute of the configuration element.

The next task at hand will be to modify the CodeGenerator.Core.LibraryConstants class. Load this class and modify the file by adding the following constants:

/// <summary>
/// Code file output root directory key
/// </summary>
public const string OUTPUT_ROOT = "OutputRoot";
/// <summary>
/// Delete procedure template path
/// </summary>
public const string TEMPLATE_PATH_DELETE_PROCEDURE = "DeleteProcedureTemplatePath";
/// <summary>
/// Insert procedure template path
/// </summary>
public const string TEMPLATE_PATH_INSERT_PROCEDURE = "InsertProcedureTemplatePath";
/// <summary>
/// Search procedure template path
/// </summary>
public const string TEMPLATE_PATH_SEARCH_PROCEDURE = "SearchProcedureTemplatePath";
/// <summary>
/// Select procedure template path
/// </summary>
public const string TEMPLATE_PATH_SELECT_PROCEDURE = "SelectProcedureTemplatePath";
/// <summary>
/// Update procedure template path
/// </summary>
public const string TEMPLATE_PATH_UPDATE_PROCEDURE = "UpdateProcedureTemplatePath";
/// <summary>
/// Stored procedures folder name
/// </summary>
public const string FOLDER_NAME_STORED_PROCEDURE = "Stored Procedures";

These are the constants that we will use against the properties being exposed by the CoreBase class. Load the CoreBase class and modify the class by adding the following properties:

/// <summary>
/// Gets OutputFolderRoot
/// </summary>
protected string OutputFolderRoot
{
	get
	{
		return ConfigurationManager.AppSettings[LibraryConstants.OUTPUT_ROOT];
	}
}
/// <summary>
/// Gets DeleteProcedureTemplatePath
/// </summary>
protected string DeleteProcedureTemplatePath
{
	get
	{
		return ConfigurationManager.AppSettings[LibraryConstants.TEMPLATE_PATH_DELETE_PROCEDURE];
	}
}
/// <summary>
/// Gets InsertProcedureTemplatePath
/// </summary>
protected string InsertProcedureTemplatePath
{
	get
	{
		return ConfigurationManager.AppSettings[LibraryConstants.TEMPLATE_PATH_INSERT_PROCEDURE];
	}
}
/// <summary>
/// Gets SearchProcedureTemplatePath
/// </summary>
protected string SearchProcedureTemplatePath
{
	get
	{
		return ConfigurationManager.AppSettings[LibraryConstants.TEMPLATE_PATH_SEARCH_PROCEDURE];
	}
}
/// <summary>
/// Gets SelectProcedureTemplatePath
/// </summary>
protected string SelectProcedureTemplatePath
{
	get
	{
		return ConfigurationManager.AppSettings[LibraryConstants.TEMPLATE_PATH_SELECT_PROCEDURE];
	}
}
/// <summary>
/// Gets UpdateProcedureTemplatePath
/// </summary>
protected string UpdateProcedureTemplatePath
{
	get
	{
		return ConfigurationManager.AppSettings[LibraryConstants.TEMPLATE_PATH_UPDATE_PROCEDURE];
	}
}

As seen in the CoreBase code we added, all new element values from the configuration file have been properly exposed by way of the CoreBase class and therefore being made available to all derived classes within the CodeGenerator.Core assembly and concludes the configuration support for the purposes of this article.

Support Enumerations

Before we create the CodeBuilder class, we will need two enumerations

  • FolderTypes
  • TemplateTypes

These enumerations will control and identify necessary generation operations and help the assembly determine paths for generated outputs. Usage of these enumerations will become more clear as we move along. Add a new class file to the CodeGenerator.Core assembly and name it "FolderTypes". When loaded in the editor, modify the file to contain the following code:

#region Using namespaces...
using System;
#endregion

namespace CodeGenerator.Core
{
	/// <summary>
	/// FolderTypes enumeration
	/// </summary>
	public enum FolderTypes
	{
		/// <summary>
		/// StoredProcedureOutput folder type enumeration member
		/// </summary>
		StoredProcedureOutput
	}
}

The next enumeration to create will be the TemplateTypes enumeration. Again, add a new class file to the CodeGenerator.Core assembly, name it "TemplateTypes" and add the following code to the file:

#region Using namespaces...
using System;
#endregion

namespace CodeGenerator.Core
{
	/// <summary>
	/// TemplateTypes enumeration
	/// </summary>
	public enum TemplateTypes
	{
		/// <summary>
		/// StoredProcedureTemplates template type enumeration member
		/// </summary>
		StoredProcedureTemplate
	}
}

All necessary support plumbing should be in place now and therefore, we are now able to move on to the CodeBuilder class.

On to Code Generation

Code generation is a process that involves many smaller steps and while it might seem like a complex process, in reality it is a very simple process depending on how the process is divided and data is organized. We begin organizing these code generation tasks with the creation of the CodeBuilder class in the CodeGenerator.Core assembly. Once loaded add the following code to the file:

#region Using namespaces...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Xml.Linq;
using System.Collections;
using System.IO;
using System.Xml.Xsl;
using System.Xml;
using System.ComponentModel;
#endregion

namespace CodeGenerator.Core
{
	/// <summary>
	/// CodeBuilder class
	/// </summary>
	public class CodeBuilder : MetadataGenerator, INotifyPropertyChanged
	{
		#region Events
		/// <summary>
		/// PropertyChanged event
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;
		#endregion

		#region Members
		/// <summary>
		/// Metadata field
		/// </summary>
		private string metadata = string.Empty;
		/// <summary>
		/// OutputMessage field
		/// </summary>
		private string outputMessage = string.Empty;
		#endregion

		#region Properties
		/// <summary>
		/// Gets Metadata
		/// </summary>
		private string Metadata
		{
			get
			{
				// Retrieve XML
				if (metadata.Equals(string.Empty))
					metadata = GetMetadataXml();
				
				return metadata;
			}
		}

		/// <summary>
		/// Gets/Sets OutputMessage
		/// </summary>
		public string OutputMessage
		{
			get
			{
				return outputMessage;
			}
			set
			{
				outputMessage = value;

				// Notify of change
				NotifyPropertyChanged(outputMessage);
			}
		}
		#endregion

		#region Constructors
		/// <summary>
		/// Default class constructor
		/// </summary>
		public CodeBuilder() { }
		#endregion

		#region Utility Methods
		/// <summary>
		/// Handles property change eventing
		/// </summary>
		/// <param name="info"></param>
		private void NotifyPropertyChanged(String info)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(info));
			}
		}		
		#endregion
	}
}

What we have done here is create the basic skeleton of the class that will be responsible for processing the combined extracted metadata and the XSL template data into useful output that will meet our code generation requirements. As seen in the class signature and class implementation, the CodeBuilder class derives from MetadataGenerator and INotifyPropertyChanged. We derive from MetadataGenerator as a way to maintain the inheritance tree within the classes in the assembly and to provide an in memory persisting mechanism for the retrieved metadata without having to access the generation target database continuously throughout the process of creating our file output. We also implement INotifyPropertyChanged as a way to provide message notifications to the application consuming our assembly about the step the code generation process is engaged in. While this isn't strictly necessary, it is a nice feature to have as it maintains the end user informed about the inner happenings of the component executing within the context of the application. For this purpose, every single time that the string property "OutputMessage" is modified, "NotifyPropertyChanged" is invoked and the "PropertyChanged" event is raised sending whatever message we want to the user about the current executing process.

The Code Generation Process

Before we begin writing the necessary code to support code generation against the metadata, we will need a series of support methods and a class that will allow us to write to the file system. We will begin by creating a static public class called "FileSystemUtility". This class will expose two static methods:

  • CreateDirectory: Will create an output folder and return its path to the caller
  • SaveTransformedSourceToFile: Will parse generated XML and output the content of the "codefile" nodes to the file system

At this point you might be thinking: "More XML and codefile nodes?". A brief explanation is in order. The job of the code generator is to make use of XSL templates that will transform XML metadata belonging to the generation target database. Once the transformation process has completed, the transformed data will be in XML format. The beauty of this approach is that prior to saving and as part of the output to disk, this generated XML data, which contains all of the files we will output, can be queried by using LINQ to XML, ripped apart and saved to files. The following schema represents the output of the XSL transformation against the database we have created on Part I of this code generation series:

<codefiles>
	<codefile filename=""><![CDATA[ ]]></codefile>
</codefiles>

As seen in the XML schema fragment, all file content will be safely stored within the CDATA section of the "codefile" element, and the generated file name of the resulting file will be stored in the "filename" attribute of the codefile element. Armed with this knowlege, we are ready to begin coding!

Add a new static class file to the CodeGenerator.Core assembly and name it "FileSystemUtility". After the class file is added, right-click on the references folder of the assembly and add a reference to the System.Web assembly. We do this because we will make use of the HttpUtility.HtmlDecode method to decode the conversion that occurs during the transformation process of characters into HTML entities. Once done with that step, add the following code to the class:

#region Using namespaces...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using System.Web;
#endregion

namespace CodeGenerator.Core
{
	/// <summary>
	/// FileSystemHandler class
	/// </summary>
	static public class FileSystemUtility
	{
		#region Utility Methods
		/// <summary>
		/// Handles the creation of output folders
		/// </summary>
		/// <param name="value"></param>
		static public string CreateDirectory(string outputFolder, FolderTypes folderType)
		{
			// Pre-allocate return value
			string retVal = string.Empty;

			// Create output root folder if it does not exist
			if (!Directory.Exists(outputFolder))
				Directory.CreateDirectory(outputFolder);

			string folderPath = string.Empty;

			// Create output folder
			switch (folderType)
			{
				case FolderTypes.StoredProcedureOutput:
					// Set path
					retVal = Path.Combine(outputFolder, LibraryConstants.FOLDER_NAME_STORED_PROCEDURE);

					// Create folder
					if (!Directory.Exists(retVal))
						Directory.CreateDirectory(retVal);
					break;
			}

			return retVal;
		}

		/// <summary>
		/// Handles saving the transformed XML into separate files to the file system
		/// </summary>
		/// <param name="source"></param>
		static public void SaveTransformedSourceToFile(string path, byte[] sourceBytes)
		{
			// Build sources from XML
			string stringFromSourceBytes = HttpUtility.HtmlDecode(Encoding.ASCII.GetString(sourceBytes));
			XElement source = XElement.Parse(stringFromSourceBytes, LoadOptions.PreserveWhitespace);

			// Retrieve codefile nodes
			List<XElement> codeFiles = (from elements in source.Descendants("codefile") select elements).ToList();

			// Parse result
			foreach (XElement element in codeFiles)
			{
				// Retrieve file name from element attribute
				string fileName = element.Attribute("filename").Value;

				// Write file to disk
				if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(fileName))
				{
					// Create file stream object
					using (FileStream stream = new FileStream(Path.Combine(path, fileName), FileMode.OpenOrCreate, FileAccess.ReadWrite))
                    {
                    	// Get data bytes
						byte[] writeData = Encoding.ASCII.GetBytes(element.Value);

						// Flush data to disk
						stream.Write(writeData, 0, writeData.Length);
						stream.Flush();
						stream.Close();
                    }
				}
			}
		}
		#endregion
	}
}

Let's examine the "CreateDirectory" method. This method will receive as a parameter the root output folder path. This is the path were all of our files will be stored. This method however does attempt to organize the output structure by also creating a sub-folder for stored procedures. This method can be further edited to include folders for many more objects as templates are created for more than just stored procedures, but C# and even UI's objects as well. For the purposes of this article however, we will only focus on generating stored procedures. Therefore, this method, though it accept a parameter of FolderTypes type, will only handle FolderTypes.StoredProcedureOutput enumeration members. Essentially, the method will simply combine the path of the output folder with the folder name of the stored procedure folder, create the path if not available, and return the path regardless of whether the path exists or not.

Let's now briefly examine the "SaveTransformedSourceToFile" method. This method will accept the path of where the generated files should be saved and a byte array that contains the HTML encoded data that we will be converting back to decoded form and then saving to file. This data, when converted back to a decoded string will look much like the XML schema shown. It is then the responsibility of this method to query the decoded XML string, get the output file name and the codefile node data and then save it to file. There really isn't much more than that to this method and therefore, we can move now to the "CodeBuilder" class that will perform the bulk of the metadata transformation required by the "SaveTransformedSourceToFile" method of the "FileSystemUtility" class.

If not already loaded, open up the CodeBuilder class. We will now add a series of support methods to the class that will allow the object to create output folders, create special data tables to store the generated XML output, retrieve XSL template paths, transform output, and act as a single entry point into the code generation process. To do this, we need to add the following methods to the class:

/// <summary>
/// Performs code generation process
/// </summary>
/// <param name="templateTypesList"></param>
public void PerformCodeGeneration(List<TemplateTypes> templateTypesList)
{
	// Retrieve codeStore
	OutputMessage = "Setting up code store data table...";
	DataTable codeStore = GetTransformCodeStore(templateTypesList);
	OutputMessage = "Code store data table setup is complete... Preparing to save data";

	// Process table data
	if (codeStore.Rows.Count > 0)
	{
		OutputMessage = "Processing generated data...";
		foreach (DataRow row in codeStore.Rows)
		{
			// Create output folder
			string outputPath = CreateOutputFolderIfNotExists((TemplateTypes)row["TemplateType"]);
			OutputMessage = string.Format("Created/Verified folder: {0}...", outputPath);

			// Save files to disk
			OutputMessage = "Preparing to save data to disk...";
			if (!string.IsNullOrEmpty(outputPath))
				FileSystemUtility.SaveTransformedSourceToFile(outputPath, (byte[])row["OutputBytes"]);

			OutputMessage = "Successfully saved generated files to disk...";
		}
	}
}

/// <summary>
/// Handles the creation of the output folder if one does not exist
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private string CreateOutputFolderIfNotExists(TemplateTypes value)
{
	// Pre-allocate return value
	string retVal = string.Empty;

	// Create output folder
	switch (value)
	{
		case TemplateTypes.StoredProcedureTemplate:
			retVal = FileSystemUtility.CreateDirectory(OutputFolderRoot, FolderTypes.StoredProcedureOutput);
			break;
	}

	return retVal;
}

/// <summary>
/// Returns a data table object that contains transformed data from metadata
/// </summary>
private DataTable GetTransformCodeStore(List<TemplateTypes> templateTypesList)
{
	// Pre-allocate return value
	DataTable codeStore = null;

	if (templateTypesList.Count > 0)
	{
		// Prepare code store
		codeStore = GetCodeStore();

		// Process template types
		OutputMessage = "Retrieving templates...";
		foreach (TemplateTypes templateType in templateTypesList)
		{
			// Retrieve template paths from configuration
			Dictionary<string, string> templatePaths = GetTemplatePaths(templateType);
			OutputMessage = "Successfully built template dictionary...\n";

			// Parse templates
			foreach (string key in templatePaths.Keys)
			{
				OutputMessage = string.Format("Verifyting {0} template...", key);
				if (File.Exists(templatePaths[key]))
				{
					// Create new row with values
					OutputMessage = "Template verified, proceeding to transform output...";
					DataRow row = codeStore.NewRow();
					row["TemplateType"] = templateType;
					row["OutputBytes"] = Encoding.ASCII.GetBytes(TransformedOutput(templatePaths[key]));

					// Add row to code store
					codeStore.Rows.Add(row);
				}
			}
		}
	}

	return codeStore;
}

/// <summary>
/// 
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private DataTable GetCodeStore()
{
	// Pre-allocate return value
	DataTable retVal = new DataTable("CodeStore");

	// Load columns
	retVal.Columns.Add(new DataColumn("TemplateType", typeof(TemplateTypes)));
	retVal.Columns.Add(new DataColumn("OutputBytes", typeof(byte[])));

	return retVal;
}

/// <summary>
/// Returns a transformed version of the metadata
/// </summary>
/// <param name="templatePath"></param>
/// <returns></returns>
private string TransformedOutput(string templatePath)
{
	// Create transform object and load template
	OutputMessage = string.Format("Loading {0}...", templatePath);
	XslCompiledTransform transform = new XslCompiledTransform(false);
	transform.Load(templatePath);

	// Create the XML reader that will contain the XML metadata extracted from the server
	XmlReader xmlReader = XmlReader.Create(new StringReader(Metadata));

	// Create XML writer settings
	XmlWriterSettings settings = new XmlWriterSettings();
	settings.CloseOutput = true;
	settings.ConformanceLevel = ConformanceLevel.Fragment;
	

	// Create the XML writer that will create the output of the XSL transformation
	StringBuilder retVal = new StringBuilder();
	XmlWriter xmlWriter = XmlWriter.Create(retVal, settings);

	// Transform the metadata into the target code object
	OutputMessage = string.Format("Generating items for {0}...", templatePath);
	transform.Transform(xmlReader, xmlWriter);
	OutputMessage = "SUCCESS!...\n";

	return retVal.ToString();
}

/// <summary>
/// Retrieves template paths
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private Dictionary<string, string> GetTemplatePaths(TemplateTypes value)
{
	// Pre-allocate return value
	Dictionary<string, string> retVal = new Dictionary<string, string>();

	// Parse template types
	switch (value)
	{
		case TemplateTypes.StoredProcedureTemplate:
			retVal.Add("SelectProcedure", SelectProcedureTemplatePath);
			break;
	}

	return retVal;
}

The following table explains what each method does:

CreateOutputFolderIfNotExists This method receives a TemplateType value that in turn, it translates form the purpose of calling the FileSystemUtility.CreateDirectory() method and returning the final output folder for the generated file.
GetCodeStore This method is responsible for creating a blank table with two columns:
  • A column for the template type.
  • A column for the transformation output in binary form.
GetTemplatePaths This method accepts a value of TemplateTypes type and in turn returns a dictionary containing the paths of the target templates as exposed by the CoreBase class.
TransformedOutput This method is primarily responsible for transforming XML metadata into generated code.
GetTransformCodeStore This method populates the data table that contains the generated code in XML format to be parsed by the FileSystemUtility class.
PerformCodeGeneration This is the only method that will interact with a calling application. This could be considered the entry point in the code generation process managed by the CodeBuilder class. It accepts a generic list of type "TemplateTypes" that will be passed down to the GetTransformCodeStore() method. For each template type, the class generates requested code.

While brief, the previous walkthroughs through the CodeBuilder class provide insight in how the code generation process works. Now that the CodeGenerator.Core assembly is complete, a quick class diagram of all classes in the assembly should resemble the following:

CodeGenerator.Core Class Diagram

CodeGenerator.Core Class Diagram

Next, we will create a basic template for a stored procedure that will be responsible for selecting all rows from a database table (the generation target database).

A Simple Template

Before continuing, while this article only covers the creation of one template, the available source code for this article does contain templates for the following stored procedures

  • Delete Procedure
  • Insert Procedure
  • Search Procedure
  • Select Procedure
  • Update Procedure

Also, it is not the intention of this article to be a tutorial on XSL programming. The template code is provided "as is" and simply because without templates, the application would not work so well. Disclaimers aside, the templates will follow a very specific structure that conforms to the metadata we have obtained from the generation target database. The templates will perform the following tasks:

  • Find the root of the metadata XML document
  • Create a series of variables to be used as shortcuts throughout the template
  • Iterate through the table elements of the metadata document
  • Create the file name of the destination file
  • Insert heading information which will include personalization data if specified to be used
  • Create the structure of the file

The following is a sample "Select All" type stored procedure template made to work with our metadata:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/metadata">

<!-- 
	Template Type: Select Stored Procedure
	Template Version: 1.0
	Created: 6/04/2009
-->

<!-- Global Variable Declarations -->
<xsl:variable name="databaseName" select="project/@database" />
<xsl:variable name="creationDate" select="project/@created" />
<xsl:variable name="namePrefix" select="project/@sqlcodeobjectprefix" />
<xsl:variable name="usePersonalData" select="project/personalization/@includepersonaldata" />
<xsl:variable name="developerName" select="project/personalization/developername" />
<xsl:variable name="companyName" select="project/personalization/company" />
<xsl:variable name="developerPosition" select="project/personalization/position" />
<xsl:variable name="developerURL" select="project/personalization/developerurl" />
<xsl:variable name="copyrightInformation" select="project/personalization/copyright" />

<!-- Object Header Information -->
<!-- -->&#60;codefiles&#62;
<xsl:for-each select="project/databasetables/table">
<xsl:variable name="objectName" select="@name" />
<xsl:variable name="objectType" select="@type" />
<xsl:variable name="parentObjectPrimaryKey" select="columns/column[@primarykeystatus='true']/@name" />
<xsl:if test="$objectType='table'">
<!-- -->&#60;codefile filename="<xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Select.sql"&#62;&#60;![CDATA[
/******************************************************************************
** Source Database: <xsl:value-of select="$databaseName" />
** Object Name: <xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Select
** Creation Date: <xsl:value-of select="$creationDate" />
<xsl:if test="$usePersonalData='true'">
** Developer Name: <xsl:value-of select="$developerName" />
** Company Name: <xsl:value-of select="$companyName" />
** Position: <xsl:value-of select="$developerPosition" />
** URL: <xsl:value-of select="$developerURL" />
** Copyrignt Information: <xsl:value-of select="$copyrightInformation" />
** Legal:
** This source code is protected by the copyright laws of the United States of 
** America and international treaties. Any unauthorized reproduction and/or
** distribution of this source code is strictly prohibited.
</xsl:if>
<!-- -->******************************************************************************/

IF EXISTS (SELECT * FROM sysobjects WHERE type = 'P' AND name = '<xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Select')
	BEGIN
		PRINT 'Dropping Procedure <xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Select'
		DROP  Procedure  [<xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Select]
	END

GO

PRINT 'Creating Procedure <xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Select'
GO

CREATE PROCEDURE dbo.[<xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Select]
AS

-- Disable affected record count
SET NOCOUNT ON

-- Select all data from <xsl:value-of select="$objectName" /> and order by primary key
SELECT * FROM [<xsl:value-of select="$objectName" />] ORDER BY <xsl:value-of select="$parentObjectPrimaryKey" />
GO

GRANT EXEC ON [<xsl:value-of select="$namePrefix" />_<xsl:value-of select="$objectName" />_Update] TO public
GO]]&#62;&#60;/codefile&#62;
</xsl:if>
</xsl:for-each>
<!-- -->&#60;/codefiles&#62;
</xsl:template>
</xsl:stylesheet>

While it may seem like a tedious task (and I often believe it is) to code these templates, the silver lining is that they only have to be coded once and simply used. If well documented, these templates are fairly simple to maintain as requirements for such templates change over time. Having now covered template structure all we have left to do is modify the CodeGenerator console application to make use of the assembly and template we have completed

Putting X, Y, Z to Work Together

It is finally time to finish the application and start generating code files. To do so, we will need to modify the Program class on the CodeGenerator console application. Load the file and add the following code to the Main() method of the class:

/// <summary>
/// Main application entry method
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
	// Signal start of code generation process
	Console.ForegroundColor = ConsoleColor.Green;
	Console.WriteLine("Starting code generation process: {0}".ToUpper(), 
		DateTime.Now.ToString());

	// Create list of templates to process
	List<TemplateTypes> templatesToProcess = new List<TemplateTypes>();
	templatesToProcess.Add(TemplateTypes.StoredProcedureTemplate);

	// Create code builder and generate code
	Console.ForegroundColor = ConsoleColor.DarkGray;
	Console.WriteLine("\nStarting code generation process...");
	CodeBuilder builder = new CodeBuilder();
	builder.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(builder_PropertyChanged);

	builder.PerformCodeGeneration(templatesToProcess);

	// Signal completion
	Console.ForegroundColor = ConsoleColor.Green;
	Console.WriteLine("\nCode generation process is complete: {0}".ToUpper(), 
		DateTime.Now.ToString());

	// Hang the console
	Console.ForegroundColor = ConsoleColor.DarkGray;
	Console.WriteLine("\n\nPress [Return] to exit...");
	Console.Read();
}

The code simply creates a list of TemplateTypes type and invokes the PerformCodeGeneration method passing the template types list along to the CodeGenerator.Core assembly. Also, as noted in the Main method with the line:

builder.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(builder_PropertyChanged);

we will need to add a static method that will handle messaging to the screen for the application as part of the "INotifyPropertyChanged" implementation of the CodeBuilder class. Therefore, add the following method to the Program class:

/// <summary>
/// Handles PropertyChanged events
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
static void builder_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
	// Output message to the console
	Console.WriteLine(e.PropertyName);
}

At this point, we are ready to build the application and launch it. If things go well, the application will display a series of messages to the screen and created the necessary output folders specified in the configuration file and all of generated files supported by the provided templates.

Conclusion

In this Code Generation series we have explored the concepts behind code generation and the tools that are available for free to make this task a possibility without having to commit to non-free and rather expensive applications available commercially. While these applications are excellent choices for their intended purpose, their purpose can indeed be achieved by simple means and moderate programming skill while maintaining full control over patterns, design and architecture.

Get the source code for this article.

Donations
Comments (0) Trackbacks (0)

No comments yet.


Leave a comment


No trackbacks yet.