Introduction
Project Templates and Item Templates are hidden gems of development in Visual Studio. They not only improve development time but also force any architectural concept specially in large projects.
This is not a first attempt to write an article on Project Template but I am trying to address whatever I developed in my recent work. Going forward, I may extend this article for future aspects.
Background
Project Templates and Item Templates provide common project structure to enterprise solution. Project Templates are very useful when you have a series of similar projects in one/many solution(s). It provides common project references, class structure, default implementation and a lot more.
Get more information about Project and Item templates here.
Using the Code
In this article, we will create a customized Project Template with Wizard Extension. You will get more on project template at MSDN site here. We will use Wizard Extension to get inputs from user at the time of creating project.
In our solution, we will assume that all projects are implementing MyProjectTemplate.BaseProject
class and IDisposable
interface:
using MyProjectTemplate.BaseProject;
namespace MyExternalService
{
public class ExternalService : BaseService, IDisposable
{
}
}
Create a Base Project Template
To create a Project Template, we require a base project template, where we are referring to external references, a customized Class, customized external files (will input files in wizard). One can extend this to various number of combinations like Config files, namespaces, etc.
We can modify this project and their classes by using project template attributes.
Create a Project
Create a project and add all required references including MyProjectTemplate.DLL.
You also need to add Request.XML and Response.XML into resources folder.
Add Default Class
Now add one class MyExternalService
to your project and implement it for BaseService
and IDisposable.
using MyProjectTemplate.BaseProject;
namespace MyExternalService
{
public class MyExternalService : BaseService, IDisposable
{
public override string Action
{
get { return "POST"; }
}
public override bool ValidateRequest(object context, object request)
{
if (!base.ValidateRequest(context, request)) return false;
return true;
}
public override int SLA
{
get { return 30; }
}
public override int Timeout
{
get { return 20; }
}
protected override string RequestXML
{
get { return "MyExternalService.Resources.Request.XML"; }
}
protected override string ResponseXML
{
get { return "MyExternalService.Resources.Response.XML"; }
}
protected override string TargetURL
{
get { return "http://localhost/MyExternalService"; }
}
public void Dispose()
{
}
}
}
Make sure that you are able to compile project so that when you will create template, your created project should not introduce any errors.
Substitute Parameters in Project
Now our project is ready to substitute parameters.
In the above class, replace namespace and class name MyExternalService
by $projectname$
. You can also replace $projectname$
in RequestXML
and ResponseXML
property so new property should refer to XML files in newly created project.
After substituting parameters in the above class, our project will look like below:
using MyProjectTemplate.BaseProject;
namespace $projectname$
{
public class $projectname$ : BaseService, IDisposable
{
public override string Action
{
get { return "POST"; }
}
public override bool ValidateRequest(object context, object request)
{
if (!base.ValidateRequest(context, request)) return false;
return true;
}
public override int SLA
{
get { return 30; }
}
public override int Timeout
{
get { return 20; }
}
protected override string RequestXML
{
get { return "$projectname$.Resources.Request.XML"; }
}
protected override string ResponseXML
{
get { return "$projectname$.Resources.Response.XML"; }
}
protected override string TargetURL
{
get { return "http://localhost/MyExternalService"; }
}
public void Dispose()
{
}
}
}
Note: Don't worry now you will not be able to compile your project but that should be fine for now.
Export Project Template
Now select this project and click on Export Template from File menu in your Visual Studio, an Export Template Wizard should appear.
Select project from drop-down in Export Template Wizard and click next.
Provide Template name, description, icon and default preview image and click on Finish button.
After exporting a project as Project Template, now your exported project should appear in Project Template lists in Add/Create project list in Visual Studio.
This project template is still basic and not taking any inputs from user like XML Files, URL, timeouts etc.
To implement more customization in Project Template, you need to use Wizard Extension, where you will create a form to enter inputs from user and eventually it will be used in Creating a New project using project template.
You can find detailed implementation @ Visual Studio 2005 Project and Item Templates.
Create a Wizard Extension
As described in the earlier section, to add User Inputs at time of creating new project, we will add Wizard Extension, which uses an input form.
To create a Wizard Extension, we have to add one Class Library project. In this exercise, we have to perform below mandatory functionality.
- Add a class inheriting from
IWizard
interface, which is part of Microsoft.VisualStudio.TemplateWizard
namespace. - Add a Windows Form and provide input fields like Open File Dialog, text box and Submit button.
- Create a strong type Assembly with a strong key and register into GAC.
Add a Property Class to store input from user.
Add a ProjectProperty
class with get
/set
properties only (DTO), this class will help to store and transfer user inputs from Form. You can customize this class for your own needs.
public class ProjectProperty
{
public string Method = "POST";
public string RequestSchemaFilePath = "";
public string RequestSchemaFile = "";
public string ResponseSchemaFilePath = "";
public string ResponseSchemaFile = "";
public int SLA = 20;
public int TimeOut = 15;
public string ExactTargetURL = @"https://MyWebservice.com/AddCustomer.asmx";
}
Add a Windows Form with all customized inputs.
Add a Windows Form with all customized controls and exposed one property of type ProjectProperty
. So when user Submits the form Wizard can take control values from exposed property.
public ProjectProperty ESInterfaceProject
{
get { return this.projectProperty; }
}
private void btnSubmit_Click(object sender, EventArgs e)
{
int.TryParse(txtSLA.Text, out this.projectProperty.SLA);
int.TryParse(txtTimeout.Text, out this.projectProperty.TimeOut);
this.projectProperty.ExactTargetURL = txtTargetURL.Text;
this.projectProperty.Method = txtMethod.Text;
this.Close();
}
Implement IWizard interface.
Add a class with implementing IWizard
interface.
namespace MyProjectTemplate
{
public class MyCustomWizard : IWizard
{
}
}
IWizard
implementing RunStarted
method, shows your Windows Form from this method and maps all customized values (ProjectProperty
) to the replacementsDictionary
collection.
public void RunStarted(object automationObject, Dictionary<string,
string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
{
try
{
inputForm = new SchemaForm();
inputForm.ShowDialog();
replacementsDictionary.Add("$path1$",
inputForm.ESInterfaceProject.RequestSchemaFilePath);
replacementsDictionary.Add("$path2$",
inputForm.ESInterfaceProject.ResponseSchemaFilePath);
replacementsDictionary.Add("$file1$",
inputForm.ESInterfaceProject.RequestSchemaFile);
replacementsDictionary.Add("$file2$",
inputForm.ESInterfaceProject.ResponseSchemaFile);
replacementsDictionary.Add("$method$",
inputForm.ESInterfaceProject.Method);
replacementsDictionary.Add("$sla$",
inputForm.ESInterfaceProject.SLA.ToString());
replacementsDictionary.Add("$timeout$",
inputForm.ESInterfaceProject.TimeOut.ToString());
replacementsDictionary.Add("$targeturl$",
inputForm.ESInterfaceProject.ExactTargetURL);
EnvDTE.DTE dte = automationObject as EnvDTE.DTE;
string solutionPath = System.IO.Path.GetDirectoryName(dte.DTE.Solution.FullName);
string projectPath = System.IO.Path.GetDirectoryName(dte.DTE.FullName);
replacementsDictionary.TryGetValue("$projectname$", out projectPath);
projectPath = solutionPath + "\\" + projectPath;
string xsdPath = projectPath + @"\Resources";
replacementsDictionary.Add("$xsdpath$",
xsdPath);
this._xsdPath = xsdPath;
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
Adding Files to Project
You cannot add files to project structure in RunStarted
method, because at time of RunStarted
method executing, project structure has been created in Temp folder and DTE is trying to read files from Temp folder which are not existing.
Another problem is that you can't create any file and folder in project directory because at the time of creating file/folder in project folder, IO will create project folder and after RunStarted
method DTE will give error because it will already have project folder, and it is assuming that project has been already created.
So the final solution is, you need to copy files after project has been created, which you can achieve by implementing RunFinished
method, which executes when DTE completing project creating task.
public void RunFinished()
{
if (!System.IO.Directory.Exists(_xsdPath))
{
System.IO.Directory.CreateDirectory(_xsdPath);
}
System.IO.File.Copy(inputForm.ESInterfaceProject.RequestSchemaFilePath, _xsdPath +
"\\" + inputForm.ESInterfaceProject.RequestSchemaFile, true);
System.IO.File.Copy(inputForm.ESInterfaceProject.ResponseSchemaFilePath, _xsdPath +
"\\" + inputForm.ESInterfaceProject.ResponseSchemaFile, true);
}
Register in GAC
After compiling the above DLL with Strong Key, register it in GAC. This is a mandatory step as project template has access of GAC.
Updating Project Template Again
Now we are ready with our customized Wizard Extension using IWizard
interface. In this section, we will add substitute parameters in our project.
After updating parameters in project template class, it will look like below:
using MyProjectTemplate.BaseProject;
namespace $projectname$
{
public class $projectname$ : BaseService, IDisposable
{
public override string Action
{
get { return "$method$"; }
}
public override bool ValidateRequest(object context, object request)
{
if (!base.ValidateRequest(context, request)) return false;
return true;
}
public override int SLA
{
get { return $sla$; }
}
public override int Timeout
{
get { return $timeout$; }
}
protected override string RequestXML
{
get { return "$projectname$.Resources.$file1$"; }
}
protected override string ResponseXML
{
get { return "$projectname$.Resources.$file2$"; }
}
protected override string TargetURL
{
get { return "$targeturl$"; }
}
public void Dispose()
{
}
}
}
Export Project
Now again, export the above project in the same way we exported in earlier section.
Updating .vstemplate File
Now update .vstemplate file and update with Wizard Extension.
Open .vstemplate file in Notepad and add the below code and save it.
<VSTemplate Version="3.0.0"
xmlns="http://schemas.microsoft.com/developer/vstemplate/2005"
Type="Project">
<TemplateData>
...
</TemplateData>
<TemplateContent>
...
</TemplateContent>
<WizardExtension>
<Assembly>MyProjectTemplate, Version=1.0.0.1,
Culture=Neutral, PublicKeyToken=27092cf962afb8d7</Assembly>
<FullClassName>MyProjectTemplate.MyCustomWizard</FullClassName>
</WizardExtension>
</VSTemplate>
Zip it and save it in My Exported Templates location.
Now zip the entire project template and .csprj files from "My Exported Templates" location.
Finally
We are almost ready with our customized Project Template with Wizard Extension. If everything is in place, then you will be able to see Project Template in Add/Create project menu in Visual Studio.
When you will create a new project, you will prompt for files and other inputs from user, which information being used to generate class and project. You might need to include Resources folder into project files manually (I am working on this part to create it automatically and will update you once I am done).
This way, you can customize N number of combinations and you can also use in Config files, XML files, etc.
Code
Coming soon.... I will update code in couple of days.