Click here to Skip to main content
15,891,253 members
Articles / Programming Languages / XML

Creating project item code templates for Visual Studio .NET

Rate me:
Please Sign up or sign in to vote.
4.86/5 (47 votes)
26 Jul 200517 min read 186.2K   2.5K   142  
This article describes how new project item code templates work in Visual Studio .NET 2003 (and 2005) and presents a component that makes it easy to write new templates.
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Collections;
using System.Xml;
using System.Xml.Schema;
using System.IO;
using System.Configuration;
using System.Security.Principal;
using System.DirectoryServices;

using Microsoft.Win32;

using EnvDTE;
//using Extensibility;

using System.Reflection;

namespace Krevera.TemplateManager
{
	/// <summary>
	/// Krevera.TemplateManager.Wizard - a class for creating project items based on templates.
	/// </summary>
	[GuidAttribute("13BCFA47-E838-4fd1-BC5C-68413CCB1C1B")]
	public class Wizard : EnvDTE.IDTWizard
	{
		#region Variables

		private PageCollection			_pages;				// List with wizard pages to display
		private StringStringDictionary	_custom_variables;	// Key=variablename, elements=values	

		private string			_sProjectname;
		private ProjectItems	_projitems;
		private string			_sNewItemDir;
		private string			_sNewItemName;
		private string			_sLangInstallDir;
		private bool			_bRunSilent = false;

		private string			_sTemplateFile = "";
		private string			_sInstallDir;

		#endregion

		/// <summary>
		/// Constructor
		/// </summary>
		public Wizard()
		{
			_pages = new PageCollection();
			_custom_variables = new StringStringDictionary();

			#region Extract install directory from the registry
			try
			{
				RegistryKey rk = Registry.LocalMachine.OpenSubKey("SOFTWARE").OpenSubKey("Krevera Software").OpenSubKey(TMConstants.RegistryKey);
				_sInstallDir = (string)rk.GetValue(@"InstallDir");
				if (_sInstallDir == null || _sInstallDir.Length == 0)
					throw new Exception("-");
			}
			catch
			{
				throw new Exception("InstallDir not found in the registry!");
			}
			#endregion
		}

		/// <summary>
		/// IDTWizard-function. Executed when a project item is added to a project
		/// </summary>
		/// <param name="Application"></param>
		/// <param name="hwndOwner"></param>
		/// <param name="ContextParams"></param>
		/// <param name="CustomParams"></param>
		/// <param name="retval"></param>
		public void					Execute(object Application, int hwndOwner, ref object[] ContextParams, ref object[] CustomParams, ref EnvDTE.wizardResult retval)
		{
			DTE env = (DTE)Application;

			string sWizardType		= (string)ContextParams[0];

			if (string.Compare(sWizardType, Constants.vsWizardNewProject, true) == 0)
			{
				// New Project wizard
				#region Get ContextParams
				string sProjectname		= (string)ContextParams[1];
				string sLocalDir		= (string)ContextParams[2];
				string sVSInstallDir	= (string)ContextParams[3];
				bool bExclusive			= (bool)ContextParams[4];
				string sSolutionName	= (string)ContextParams[5];
				bool bRunSilent			= (bool)ContextParams[6];	// Don't display a GUI
				#endregion

				// New projects are not supported yet...
				retval = wizardResult.wizardResultFailure;
			}
			else if (string.Compare(sWizardType, Constants.vsWizardAddItem, true) == 0 )
			{
				// New Project Item wizard
				#region Get ContextParams
				_sProjectname		= (string)ContextParams[1];
				_projitems			= (ProjectItems)ContextParams[2];
				_sNewItemDir		= (string)ContextParams[3];
				_sNewItemName		= (string)ContextParams[4];
				_sLangInstallDir	= (string)ContextParams[5];

				if (ContextParams.GetUpperBound(0) >= 6)			// In C++, there's no run silent parameter
					_bRunSilent			= (bool)ContextParams[6];	// Don't display a GUI

				#endregion
				#region Get CustomParams
				foreach( string sParam in CustomParams)
				{
					int eqpos = sParam.IndexOf("=");
					if (eqpos < 0)
						throw new Exception("Bad parameter format. Expected [Key]=[Value]. Got\n\n" + sParam);

					string sKey = sParam.Substring(0,eqpos).Trim();
					string sValue = sParam.Substring(eqpos+1).Trim();

					switch (sKey)
					{
						#region case "TEMPLATE_FILE" - full path to the template file to use
						case "TEMPLATE_FILE":
							_sTemplateFile = ProcessString(sValue);
							break;
						#endregion
					}
					
				}
				#endregion

				#region Insert new item from template file
				ProjectItem newfile;
				newfile = _projitems.AddFromTemplate( _sTemplateFile, _sNewItemName);
				#endregion

				#region Open file and retrieve its TextDocument
				Window w = newfile.Open(Constants.vsViewKindTextView);
				TextWindow tw = (TextWindow)w.Object;

				Document d = w.Document;
				TextDocument td = (TextDocument)d.Object("TextDocument");
				#endregion

				retval = ProcessDocument(td);

				// Open file so that the user can start editing it
				env.ExecuteCommand("File.OpenFile", "\"" + td.Parent.FullName + "\"");
			}
			else
			{
				retval = wizardResult.wizardResultFailure;
			}
		}

		/// <summary>
		/// Process all template directives in an opened document
		//	and replace them with text.
		/// </summary>
		/// <param name="td">document to process</param>
		/// <returns>Wizard result value</returns>
		private EnvDTE.wizardResult ProcessDocument(TextDocument td)
		{
			EditPoint startpoint = td.StartPoint.CreateEditPoint();
			EditPoint innerstartpoint = null;
			EditPoint endpoint = null;
			EditPoint innerendpoint = null;

			try
			{

				// Find start of directive block [% .... %]
				TextRanges ranges = null;
				while (startpoint.FindPattern("[%", 0, ref innerstartpoint, ref ranges))
				{
					// Find end of block
					innerendpoint = innerstartpoint.CreateEditPoint();
					if (!innerendpoint.FindPattern("%]", 0, ref endpoint, ref ranges))
						throw new Exception("Mismatched directive block! No end marker %] found.");

					// Extract directive text from the block
					string directive = innerstartpoint.GetText(innerendpoint);
					directive = directive.Trim();

					string newstring = EvaluateFragment(directive);

					// Replace the directive
					startpoint.Delete(endpoint);
					startpoint.Insert(newstring);


					startpoint.MoveToPoint(endpoint);
				}

				return wizardResult.wizardResultSuccess;
			}
			catch (Exception exc)
			{
				MessageBox.Show(exc.ToString());
				return wizardResult.wizardResultCancel;
			}
		}

		/// <summary>
		/// Evaluate all variables in an string and replace them with the evaluated results.
		/// </summary>
		/// <param name="s">string to process. Variables can occur inside [%...%]</param>
		/// <returns>New string, with value replacements carried out.</returns>
		private string				ProcessString(string s)
		{
			try
			{

				// Find start of directive block [% .... %]
				while (s.IndexOf("[%") >= 0)
				{
					int startix			= s.IndexOf("[%");
					int innerstartix	= startix +2;
					int innerendix		= s.IndexOf("%]");
					int endix			= innerendix +2;

					if (innerendix < 0)
						throw new Exception("Mismatched directive block! No end marker %] found in '" + s + "'!");

					// Extract directive text from the block
					string directive = s.Substring(innerstartix, innerendix-innerstartix ) ;
					directive = directive.Trim();

					string newstring = EvaluateFragment(directive);

					// Replace the directive
					s = s.Remove( startix, endix-startix);
					s = s.Insert( startix, newstring);
				}

				return s;
			}
			catch (Exception exc)
			{
				MessageBox.Show(exc.ToString());
				return s;
			}
		}

		/// <summary>
		/// Process an XML fragment. If the fragment contains a wizard definition
		/// then a dialog with wizard pages is displayed where the user can give the
		/// values for custom variables. In this case the return value will be an empty
		/// string.
		/// 
		/// If the fragment contains a variable reference, then the value of the variable
		/// is returned.
		/// </summary>
		/// <param name="xmlfragment">Fragment to translate</param>
		/// <returns>Variable value, or empty string if the fragment was a wizard
		/// definition.</returns>
		private string				EvaluateFragment(string xmlfragment)
		{
			string replacement_string = "";

			StringReader sr = new StringReader(xmlfragment);
			XmlTextReader tr = new XmlTextReader(sr);

			XmlValidatingReader vr = new XmlValidatingReader(tr);

			
			vr.Schemas.Add("", Path.Combine( _sInstallDir, @"Schemas\schema.xsd"));

			vr.ValidationType = ValidationType.Schema;
			vr.ValidationEventHandler += new ValidationEventHandler (ValidationHandler);

			bool firstnode = true;
			Page curpage = null;
			bool bShowWizard = false;

			// Read the textfragment through the validating XML parser and handle all elements
			// that we come across.
			while(vr.Read())
			{
				switch (vr.NodeType)
				{
					#region case XmlNodeType.Element - start tags
					case XmlNodeType.Element:
						switch (vr.Name)
						{
							#region case wizard - root element for wizard pages
							case "wizard":
								bShowWizard = true;
								break;
							#endregion
							#region case page - defined as wizard page"
							case "page":
								curpage = new Page();
								break;
							#endregion
							#region case title - page title
							case "title":
								curpage.title = vr.ReadString();
								break;
							#endregion
							#region case description - variable description
							case "description":
								vr.MoveToContent();
								curpage.description = vr.ReadString();
								break;
							#endregion
							#region case variable - either a page variable, or if root, variable to evaluate
							case "variable":
								if (firstnode)
								{
									// The element is the root element

									// Return custom attribute
									replacement_string = EvaluateVariable( vr.GetAttribute("type"), vr.GetAttribute("name") );
								}
								else
								{
									curpage.variablename = vr.GetAttribute("name");
								}

								break;
							#endregion
						}
						break;
					#endregion
					#region case XmlNodeType.EndElement - end tags
					case XmlNodeType.EndElement:
						switch (vr.Name)
						{
							// When we reach the end of page, then we store the collected page info
							case "page":
								_pages.Add(curpage);
								curpage = null;
								break;
						}
						break;
					#endregion
				}
				firstnode = false;
			}


			#region Display wizard to create values in _custom_variables
			if (bShowWizard) 
			{
				frmPage dlg = new frmPage();

				dlg.Pages = _pages;
				dlg.CustomVariables = _custom_variables;
			
				DialogResult dlgresult = dlg.ShowDialog();
				if (dlgresult != DialogResult.OK)
				{
					throw new Exception("Wizard cancelled");
				}

			}
			#endregion

			return replacement_string;
		}

		/// <summary>
		/// Evaluate a variable and return its value
		/// </summary>
		/// <param name="vartype">The type of the variable {standard, environment, custom}</param>
		/// <param name="varname">The variable's name</param>
		/// <returns>The value of the variable</returns>
		private string				EvaluateVariable(string vartype, string varname)
		{
			string result = "";

			switch (vartype)
			{
				#region case standard - predefined variables
				case "standard":
					switch (varname)
					{
						#region case fullfilename - absolute path to the new item
						case "fullfilename":
							result = Path.Combine(_sNewItemDir, _sNewItemName);
							break;
						#endregion
						#region case filename - filename including extension
						case "filename":
							result = Path.GetFileName(_sNewItemName);
							break;
						#endregion
						#region case filenamebase - filename exluding extension
						case "filenamebase":
							result = Path.GetFileNameWithoutExtension(_sNewItemName);
							break;
						#endregion
						#region case directory - absolute path to the directory the new item is placed in
						case "directory":
							result = _sNewItemDir;
							break;
						#endregion
						#region case fullusername - full name of the current user, i.e. "Emil �str�m"
						case "fullusername":
//							if (prin != null)
//								result = GetFullName(prin.Identity.Name);
							result = GetFullName(System.Environment.UserDomainName + "\\" + System.Environment.UserName);
							break;
						#endregion
						#region case namespace - the default namespace of the project
						case "namespace":
							#region Properties and sample values
							// foreach(EnvDTE.Property p in _projitems.ContainingProject.Properties)
							//		System.Diagnostics.Trace.WriteLine( p.Name + " = " +  p.Value.ToString())
							//
							// ActiveFileSharePath = 
							// ApplicationIcon = App.ico
							// AspnetVersion = 
							// AssemblyKeyContainerName = 
							// AssemblyName = CSDatabaseTest
							// AssemblyOriginatorKeyFile = 
							// AssemblyOriginatorKeyMode = 0
							// DefaultClientScript = 0
							// DefaultHTMLPageLayout = 1
							// DefaultNamespace = CSDatabaseTest
							// DefaultTargetSchema = 1
							// DelaySign = False
							// FileName = CSDatabaseTest.csproj
							// FileSharePath = 
							// FullPath = C:\Documents and Settings\Emil\My Documents\Visual Studio Projects\CSDatabaseTest\
							// LinkRepair = False
							// LocalPath = C:\Documents and Settings\Emil\My Documents\Visual Studio Projects\CSDatabaseTest\
							// OfflineURL = 
							// OptionCompare = 0
							// OptionExplicit = 1
							// OptionStrict = 0
							// OutputFileName = CSDatabaseTest.exe
							// OutputType = 0
							// PostBuildEvent = 
							// PreBuildEvent = 
							// ProjectType = 0
							// ReferencePath = 
							// RootNamespace = CSDatabaseTest
							// RunPostBuildEvent = 1
							// ServerExtensionsVersion = 
							// StartupObject = 
							// URL = file:///C:\Documents and Settings\Emil\My Documents\Visual Studio Projects\CSDatabaseTest\
							// WebAccessMethod = 0
							// WebServer = C:\Documents and Settings\Emil\My Documents\Visual Studio Projects\CSDatabaseTest\
							// WebServerVersion = 
							#endregion

							try 
							{
								EnvDTE.Property prop = _projitems.ContainingProject.Properties.Item("DefaultNamespace");
								result = prop.Value as String;
							}
							catch {}

							break;
						#endregion
						#region case domainname - the name of the curernt domain
						case "domainname":
							result = System.Environment.UserDomainName;
							break;
						#endregion
						#region case username - short username, i.e. "emil"
						case "username":
							result = System.Environment.UserName;
							break;
						#endregion
						#region case domainusername - domain-qualified username, i.e. "domain\emil"
						case "domainusername":
//							result = System.Environment.UserDomainName + "\\" + System.Environment.UserName;
							WindowsIdentity myIdentity = WindowsIdentity.GetCurrent();
							result = myIdentity.Name;
							break;
						#endregion
						#region case machinename - name of the current computer
						case "machinename":
							result = System.Environment.MachineName;
							break;
						#endregion
						#region case projectname - name of the current project
						case "projectname":
							result = _sProjectname;
							break;
						#endregion
						#region case curdate - current date
						case "curdate":
							result = DateTime.Today.ToShortDateString();
							break;
						#endregion
						#region case installdir - template manager installation directory
						case "installdir":
							result = _sInstallDir;
							break;
						#endregion

						default:
							result = "[Unknown standard variable: " + varname + "]";
							break;
					}
					break;
				#endregion
				#region case environment - environment variables
				case "environment":
					result = System.Environment.GetEnvironmentVariable(varname);
					if (result == null)
						result = "[Environment variable not found: " + varname + "]";

					break;
				#endregion
				#region case custom - variables whose values have been requested from the user in wizard pages
				case "custom":
					result = (string)_custom_variables[varname];
					break;
				#endregion
			}

			return result;
		}

		/// <summary>
		/// Retrieve the full name for a user. NOTE: This code only work if the computer
		/// is connected to a domain network.
		/// </summary>
		/// <param name="strLogin">User name with domain. Format DOMAIN\USER</param>
		/// <returns>Full name</returns>
		private string				GetFullName(string strLogin)
		{
			string str = "";
			// Parse the string to check if domain name is present.
			int idx = strLogin.IndexOf('\\');
			if (idx == -1)
			{
				idx = strLogin.IndexOf('@');
			}

			string strDomain;
			string strName;

			if (idx != -1)
			{
				strDomain = strLogin.Substring(0, idx);
				strName = strLogin.Substring(idx+1);
			}
			else
			{
				strDomain = Environment.MachineName;
				strName = strLogin;
			}

			DirectoryEntry obDirEntry = null;

			obDirEntry = new DirectoryEntry("WinNT://" + strDomain + "/" + strName);
			System.DirectoryServices.PropertyCollection  coll = obDirEntry.Properties;

			#region Property tests
//			foreach( string n in coll.PropertyNames)
//			{
//				System.Diagnostics.Trace.WriteLine(n + " = ");
//				System.DirectoryServices.PropertyValueCollection vals=coll[n];
//				foreach (object val in vals)
//				{
//					System.Diagnostics.Trace.WriteLine(val);
//				}
//				
//			}
				// UserFlags = 
				// 66049
				// MaxStorage = 
				// -1
				// PasswordAge = 
				// 15443273
				// PasswordExpired = 
				// 0
				// LoginHours = 
				// System.Byte[]
				// FullName = 
				// 
				// Description = 
				// 
				// BadPasswordAttempts = 
				// 0
				// LastLogin = 
				// 2005-02-06 11:14:55
				// HomeDirectory = 
				// 
				// LoginScript = 
				// 
				// Profile = 
				// 
				// HomeDirDrive = 
				// 
				// Parameters = 
				// 
				// PrimaryGroupID = 
				// 513
				// Name = 
				// Emil
				// MinPasswordLength = 
				// 0
				// MaxPasswordAge = 
				// 3710851
				// MinPasswordAge = 
				// 0
				// PasswordHistoryLength = 
				// 0
				// AutoUnlockInterval = 
				// 1800
				// LockoutObservationInterval = 
				// 1800
				// MaxBadPasswordsAllowed = 
				// 0
				// RasPermissions = 
				// 1
				// objectSid = 
				// System.Byte[]
			#endregion

			object obVal = coll["FullName"].Value;
			str = obVal.ToString();

			return str;
		}

		/// <summary>
		/// Called when an XML-fragment contains errors. The only thing we do is
		/// display the error to the user so that he or she can fix it.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="args"></param>
		private static void			ValidationHandler(object sender, System.Xml.Schema.ValidationEventArgs args)
		{
			string sMsg = args.Severity + " " + args.Message;
			throw new Exception(sMsg);
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Sweden Sweden
Worked in the industry since 1996, as a developer, architect, project leader, etc.

Current interests include DirectX, Visual Studio adaptation (add-ins, macros, etc), C# and C++.

Comments and Discussions