#Visual
Studio Extension #Project Template #IWizard #VSIX
▌Introduction
Project
template is commonly used in Team’s developing process. Building a template,
for me, is not easy when the Wizard is included.
So I wrote
this article to help those who want to help your team to finish their coding
jobs with your template.
The example is
building a project template which can import DAOs (Database entities) and their
CRUD services (or repositories) under the UOW (Unit Of Work) framework.
Here are the
screenshots when I was creating a new Visual Studio project with my UOW project
template.
The project
was created with “DbContext”, “DAOs” and “Services” without writing any codes
in the following picture.
▋Related articles
▌Background
Recently, I was
working with a team and one of the team members said
“So you built
the UOW (Unit Of Work) library project, but we still don’t have the idea how to
implement our codes with it.”
Of course I
answered “Take a look at my unit tests, or my other implement codes of the
library.”
But finally I
found it was so embarrassing that everyone just keep using the original ADO.NET’s
codes and never ask me about the UOW again.
Then I am
thinking that creating a Visual Studio project template will be the best
solution to this situation. So I started to learn how to create a COMPLETED and
EASY-TO-USE project template in Visual Studio with a GUI (Wizard).
▌Environment
l
Visual Studio 2015 Ent.
▌Implement the Project Template
▋Project Template
I created a
project that will be the Project Template,
I separated
the structure into three parts :
1. Data Access Object
2. Service
3. DbContext
Data Access Object
The class of
DAO will inherit the base UOW class : UowEntity
The number of
DAO is not sure in the design time, so I will leave this folder for empty, and
will dynamically add the DAO class programmatically.
Service
The same idea
with DAOs, the class of Service will inherit the UOW base class : BaseService.
BaseService
already implemented the CRUD functions.
Since the
classes of Service are also count on the DAOs created in the run time. So I
leave the folder empty as well.
DbContext
DbContext
inherits System.Data.Entity.DbContext, which is from Entity Framework.
A project
contains one DbContext, so the DbContext can be created in this template
project like this :
namespace $projectname$.DbContext
{
public class $dbContext$ :
System.Data.Entity.DbContext
{
public $dbContext$() : base("name=$dbContext$")
{
}
}
}
|
$projectname$ : The default parameter
of Project Template, and will be replaced with the project name while users
create their new project. You don’t need to assign the value to this parameter
in the later Wizard program.
$dbContext$ : This is my customized
parameter. I have to assign a value from the input of the Wizard by user.
▌Implement the Wizard UI
Now add a new
Visual Studio project in your template project solution.
We will
implement the GUI and codes for generating dynamic files (Class) of Wizard in
this new project. I will name the project as Wizard project in the following
article.
▋The GUI for project template
Create a
Winform (or WPF) inside the Wizard project ,
The following
inputs provide the database information, in order to get the Tables in the
database and put them into the CheckListBox.
The tables selected, and the DbContext name
are the parameters for the
Template Project!
I will focus on how to make this Wizard Project providing information (values)
to the parameters of the Template Project, so the codes of database connection
or getting tables , are skipped in the article.
First we
create the DTO : ProjectProperty
to save the information
of the Wizard,
public class ProjectProperty
{
public String DbContextName { get; set; }
public List<String> EntityNames { get; set; }
public ProjectProperty()
{
this.EntityNames = new List<string>();
}
}
|
In the Submit function
(which is triggered by the OK button) in Winform.cs, initialize the object : ProjectProperty
public partial class WizardForm : Form
{
private void btSubmit_Click(object sender, EventArgs e)
{
//Validation
StringBuilder validationFailMsg
= new StringBuilder(100);
if(String.IsNullOrEmpty(tbDbContextName.Text)
|| tbDbContextName.Text.Contains(" "))
{
validationFailMsg.AppendLine("DbContext
name cannot be empty or blank inside.");
}
if (validationFailMsg.Length > 0)
{
MessageBox.Show($"Warning!\n{validationFailMsg.ToString()}");
return;
}
//Initialize the object :
ProjectProperty
ProjProp.DbContextName =
tbDbContextName.Text;
for(int i =0;
i<clbEntities.Items.Count; i++)
{
if(clbEntities.GetItemChecked(i))
{
ProjProp.EntityNames.Add((String)clbEntities.Items[i]);
}
}
this.Close();
}
}
|
▋Implement Microsoft.VisualStudio.TemplateWizard.IWizard
Create a class
to implement Microsoft.VisualStudio.TemplateWizard.IWizard
using
Microsoft.VisualStudio.TemplateWizard;
public class ProjectWizard : IWizard
{
private String _projPath = String.Empty; //Project root folder's full path
private String _projCsprojPath =
String.Empty; //The full path of
*.csproj
private String _dirPathDao = String.Empty; //DAO's folder's full path
private String _dirPathDaoService = String.Empty; //DAO Service folder's full path
private String _projName = String.Empty; //Project Name
private WizardForm _wizardForm = new WizardForm();
}
|
There are five
methods should be implement with the interface.
1. BeforeOpeningFile : void
2. ProjectFinishedGenerating : void
3. RunFinished : void
4. RunStarted : void
5. ShouldAddProjectItem : bool
We will use
the last 3 methods.
l
ShouldAddProjectItem
public bool
ShouldAddProjectItem(string filePath)
{
return true;
}
|
l
RunStarted
public void RunStarted(object automationObject,
Dictionary<string, string>
replacementsDictionary, WizardRunKind runKind, object[] customParams)
{
_wizardForm.ShowDialog();
try
{
//Add custom parameters.
replacementsDictionary.Add("$dbContext$",
_wizardForm.ProjProp.DbContextName);
//Get the inforamtion of physical
Project level
String projName = replacementsDictionary["$projectname$"];
String fullProjectPath =
replacementsDictionary["$destinationdirectory$"];
this._projName = projName;
this._projPath = fullProjectPath;
this._projCsprojPath = Path.Combine(fullProjectPath,
String.Format("{0}.csproj", projName));
this._dirPathDao = Path.Combine(fullProjectPath,
"DAO");
this._dirPathDaoService = Path.Combine(fullProjectPath, "Service");
}
catch (Exception ex)
{
MessageBox.Show(String.Format("Wizard Error on RunStarted :
{0}",
ex.Message));
}
}
|
The key points:
1. Show the GUI in this method.
2. Use replacementsDictionary : Dictionary<string, string> to collect the parameters’ key and
value.
As I mentioned before, no need to assign the value to $projectname$.
The default project’s parameter will be saved into replacementsDictionary automatically, such as $projectname$ , $destinationdirectory$.
As I mentioned before, no need to assign the value to $projectname$.
The default project’s parameter will be saved into replacementsDictionary automatically, such as $projectname$ , $destinationdirectory$.
So I can directly get the values of replacementsDictionary["$projectname$"] and replacementsDictionary["$destinationdirectory$"].
Now we get
enough information about user’s new project. In the next method : RunFinished ,
the DAO and
Service class are going to be generated dynamically.
l
RunFinished
public void RunFinished()
{
try
{
//If the directories of \DAO and
\Service not exist, create them.
this.generateDir();
//Generate the DAO and Service
classes
if (!String.IsNullOrEmpty(this._dirPathDao)
&& !String.IsNullOrEmpty(this._dirPathDao))
{
this.generateClass(this._dirPathDao);
}
//Put the .cs files into user’s
project build scope.
this.addClassToCsproj();
}
catch (Exception ex)
{
MessageBox.Show(String.Format("Wizard Error on RunFinished :
{0}",
ex.Message));
}
}
|
Okay, the idea
is simple. We want to create the classes of DAO and Service dynamically.
So we have to
1. Make sure the directories exist
2. Create them. (*.cs) in the correct directories.
3. Write codes inside of the classes.
4. Include them into user’s project.
I will show
the codes from Step 2 to 4.
The following
codes will generate the classes and the codes to inherit UOW’s base entity.
private void generateClass()
{
try
{
//Create .cs
foreach (var entityName in this._wizardForm.ProjProp.EntityNames)
{
var daoClassName = entityName;
var daoServiceClassName = $"{entityName}Service";
var daoPath = Path.Combine(this._dirPathDao, $"{daoClassName}.cs");
var daoServicePath = Path.Combine(this._dirPathDaoService,
$"{daoServiceClassName}.cs");
//Create & Write template codes
inside
this.createDaoClass(daoPath, this._projName,
daoClassName);
this.createDaoServiceClass(daoServicePath,
this._projName,
daoClassName, daoServiceClassName);
}
}
catch (Exception)
{
throw;
}
}
private void createDaoClass(String daoPath, String projName, String entityName)
{
var sbTemplate = new StringBuilder(500);
sbTemplate.AppendLine("using
System;");
sbTemplate.AppendLine("using
System.Collections.Generic;");
sbTemplate.AppendLine("using
System.Linq;");
sbTemplate.AppendLine("using
System.Text;");
sbTemplate.AppendLine("using
System.Threading.Tasks;");
sbTemplate.AppendLine("");
sbTemplate.AppendLine($"namespace {projName}.DAO");
sbTemplate.AppendLine("{ ");
sbTemplate.AppendLine($" public class {entityName} :
JB.Production.Infra.Utility.EF.Entity.UowEntity");
sbTemplate.AppendLine(" {");
sbTemplate.AppendLine(" }");
sbTemplate.AppendLine("}");
try
{
using (Stream fs = System.IO.File.Create(daoPath))
{
StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.Unicode);
sw.WriteLine(sbTemplate.ToString()); //寫入
sw.Close();
sw.Dispose();
}
}
catch (Exception)
{
throw; }
finally
{
sbTemplate.Clear();
sbTemplate = null;
}
}
private void
createDaoServiceClass(String daoServicePath, String projName, String entityname, String serviceName)
{
//Same flow with
createDaoClass
}
|
Most important
, the classes (*.cs) created programmatically won’t be included in user’s
project. We have to do it , yes, programmatically.
So take a look
a look at an usual *.csproj , and the content below declare that the *.cs are
included in a project. We will do the same thing.
private void
addClassToCsproj()
{
try
{
var p = new Microsoft.Build.Evaluation.Project(this._projCsprojPath);
DirectoryInfo diDao = new DirectoryInfo(this._dirPathDao);
foreach (var file in diDao.GetFiles("*.cs"))
{
p.AddItem("Compile", $@"DAO\{file.Name}");
}
DirectoryInfo diDaoService = new DirectoryInfo(this._dirPathDaoService);
foreach (var file in diDaoService.GetFiles("*.cs"))
{
p.AddItem("Compile", $@"Service\{file.Name}");
}
p.Save();
p.Build();
diDao = null;
diDaoService = null;
p = null;
}
catch (Exception)
{
throw;
}
}
|
▌Before using the Project Template
▋Export the Project Template and register the
Wizard
Export the Template Project
Click
【File】→ 【Export
Template】to export the project template to your HD.
The
initial path is C:\Users\...\Documents\Visual
Studio 2015\My Exported Templates\
We
will later modify the content inside the ZIP.
So
in the meantime, don’t put it in
C:\Users\...\Documents\Visual Studio
2015\Templates\
▋Signing the
assembly of Wizard Project
In
the properties of Wizard Project, go to 「Signing」and sign an assembly.
If
everything is fine, you will get a Certification Information file :
AssemblyName.pfx
,
which now is included in the Wizard Project.
It
will be used to get the Public Key later.
▋Install the
Assembly into the Global Assembly Cache
Since you assign the Wizard Project’s assembly, now
we can register it in GAC (Global Assembly Cache). There are a few ways to
arrive the goal, you can look the MSDN for
reference.
I
will use the VS command line to do this.
In
Visual Studio Developer Command Line, redirect to the directory of Wizard
Project’s assembly, such as …\bin\Debug\
Run
this command :
gacutil -i JB.Product.ProjectTemplate.EF.Wizard.dll
|
Okay
, now it is in GAC, so we can use the assembly (Wizard) in our Template
Project. Just one thing left, we have to
tell the Template Project that which Wizard’s assembly it should use!
▋Get the Public Key
of Wizard Project
We
are going to use Microsoft .NET Strong
Name Utility (Strong Name tool)
(The reference path : C:\Program
Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\sn.exe )
To make a strong name key of the Wizard Project.
You can either use the command line to get it, or add the Strong
Name tool to the Visual Studio IDE by the following steps.
In Visual Studio , go to 【Tools】→ 【External
Tools …】
And add the Strong Name tool
and command as a new one, and enable “Use output window”.
Build (Compiler) the Wizard Project, and click “Get
PublicKey Token”
You can find your public key in the output window.
Copy it, we have to write it to our Template
Project output.
▋Tell the Template
Project to use the Wizard!
Come back to the exported file (*.zip) of the
Template Project.
Unzip it, and open the file : MyTemplate.vstemplate
Add the following XML in the last part but before </VSTemplate>
<WizardExtension>
<Assembly>JB.Product.ProjectTemplate.EF.Wizard,
Version=1.0.0.8,
Culture=Neutral, PublicKeyToken=91d1bda6a14bee41</Assembly>
<FullClassName>JB.Product.ProjectTemplate.EF.Wizard.ProjectWizard</FullClassName>
</WizardExtension>
</VSTemplate>
|
The words with yellow backbgroud-color are the
settings of your own Wizard Project.
The FullClassName means the class that implemented Microsoft.VisualStudio.TemplateWizard.IWizard.
▌Using the Project Template
▋The final Project
Template
The last step! Zip all the files in the folder.
Then put it into the Template folder of Visual Studio. By
default , it will be C:\Users\...\Documents\Visual Studio 2015\Templates\
▋Enjoy it !
Congrate! You can use it in your
Visual Studio ! Enjoy coding J
In the next article, I will
introduce how to use the VSIX project template to create a Visual Studio Extension
of our Project Template; so your project template can be distributed to your
team or the world.
▌Reference
沒有留言:
張貼留言