2015年11月19日 星期四

[Visual Studio 2015] Create and distribute a Poject Template with Wizard (1)

 #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.

 


▌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$.

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 FileExport Templateto 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 Signingand 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 ToolsExternal 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





沒有留言:

張貼留言