January 17, 2017 Create a Multi-Layered Project with Visual Studio IDE Many organizations struggle with governance on software projects and look for process improvement tools to help. We demo how to build a project template. Deyan Stoynov When managing development, many organizations struggle with governance and look for process improvement tools to add value. Project templates take the guesswork out of managing the development process increasing consistency and decreasing delivery times. They are useful when you want to create many different multi-project solutions with the same file structure or standardize projects within your organization. Why Work with Project Templates? Every time we start a new project by using the New Project dialog box within the Visual Studio the files are immediately visible, generated from the project template. The item template is the little piece of code created when new classes or interfaces are developed. These templates are useful if you want to create files in any projects with the same code structure. Work More Effectively with Distributed Teams Why businesses need remote Agile teams & questions to ask before starting. Process Improvement Tools in Visual Studio Using Third Party Templates or Building Them Within Visual Studio, the are a lot of online templates in “Extensions and Updates” gallery, but one of the biggest collection of templates can be found in SideWaffle Template Pack project. The project is 100% community-driven and templates are available for almost everything: from Azure WebJobs to Angular. You can create your own SideWaffle template and upload it to the project. After approval, it will be available in next version of the extension for others to use. Creating a Project or Item Template from Existing Code If you have a ‘role model’ solution or file, it’s possible to create a template from this existing code. Use “File/Export Template…” Click to expand any image in the post to see more detail. From the wizard, you can pick a single file (for the item template) or project(s). The exported template is located in: {user}\Documents\Visual Studio {version}\My Exported Templates\{templateName}.zip. Customize Existing Templates Used in the Visual Studio It is possible to update all the templates already built in Visual Studio. If you want to change the default new class code or the default api controller, your template can be located in: \Program Files (x86)\Microsoft Visual Studio {version}\Common7\IDE\ItemTemplates\. After saving the updated template file, clear the template cache so your changes are available the next time you run Visual Studio. Simply open command prompt window as administrator and execute: devenv /installvstemplates. Create a Project or Item Template from Scratch Open Visual Studio and from New Project menu select: \Visual C#\Extensibility. The files within the project for both item and project template are similar. There are the template files from which the output file will be generated and the *.vstemplate file where all data for the template is set (name, description, icon, category etc.). Win at Distributed Software Development Why businesses need remote Agile teams & questions to ask before starting. Components in Template Projects Understanding the *.Vstemplate XML File This file contains two required tags <TemplateData> and <TemplateContent>. The TemplateData tag contains the name, description, icon, category, project type and other metadata about your template. For example, if your template is made for Visual Basic then ProjectType tag will look like <ProjectType>VB</ProjectType> or <ProjectType>CSharp</ProjectType> for C#. The TemplateContent tag contains all files that will be generated during the creation of new project or item. Working with the Template Base File These files contain a lot of tokens with dollar signs around them. The tokens will be replaced with actual values during generation of the file. For example, the $safeitemrootname$ will be replaced with the name provided by the user in the Add New Item dialog box, with all unsafe characters and spaces removed. The list with all parameters can be found here. You can define your own parameters and use them in the file but we will cover this later. Process Improvement Tools at Work: Demo Tutorial Recently our team (6 developers) started a brand new project. (Let’s call it Project X). The development lead created a solution with 6 assemblies bundles: ProjectX.Web – MVC administration website ProjectX.Api – REST WebAPI project which provides data to the MVC website ProjectX.Core – The project contains enums, helpers, extensions and interfaces for the data and business layer classes. ProjectX.Models – Here we store all domain and business models. ProjectX.Data – The DAL classes implementations live here. ProjectX.Business – BLL classes implementations live here. The architecture looks like this: Common Team Build Problems Each team member needed to develop a module, and usually one module operates 1-2 domain objects. Each domain has a repository and service classes. We use the IoC (Inversion of Control) pattern. That means each repository and service implements an interface. For each domain object, the developer needed to create 5 classes. Because we are a team of 6, this resulted in the following problems: Frequent merge conflicts in *.proj files The same methods were named differently in the services or the repositories. For example: Add() vs Insert() vs Create() If 3 domain objects must be created, developers must create 15 classes in 15 files spread through 4 projects. This tedious work can result in many copy-paste errors mainly in the documentation of these classes. Process Improvement Tools: Steps to Create an Item Template Building an item template can help teams avoid merge conflicts, naming challenges and tedious copy/paste errors. We demo the code used in each phase to: Define several template files in single Item Template project. Inject custom logic during generating of the files and distribute them across different projects. Wrap up everything in VSIX package for easy use by all team members. Strategies to Succeed Using Distributed Teams Why businesses need remote Agile teams & questions to ask before starting. Create and Define Your Template Classes Start a new project, select C# Item template, and name it ModelEx. After that, remove the default Class.cs, and add new class DomainModel.cs: namespace $rootnamespace$ { /// <summary> /// The $itemname$ domain class. /// </summary> public class $safeitemrootname$ { /// <summary> /// Gets or sets the identifier. /// </summary> public long Id { get; set; } } } Now that we have a basic domain object, we must create the classes for the Service and for the Repository. Add four more class files and name them IModelService, ModelService, IModelRepository and ModelRepository. For these template files to be generated, we need to register them in our ModelEx.vstemplate file. This is done with the following change: <ProjectItem SubType="Code" TargetFileName="$fileinputname$.cs" ReplaceParameters="true"> DomainModel.cs </ProjectItem> <ProjectItem SubType="Code" TargetFileName="I$fileinputname$Repository.cs" ReplaceParameters="true"> IModelRepository.cs </ProjectItem> <ProjectItem SubType="Code" TargetFileName="$fileinputname$Repository.cs" ReplaceParameters="true"> ModelRepository.cs </ProjectItem> <ProjectItem SubType="Code" TargetFileName="$fileinputname$Service.cs" ReplaceParameters="true"> ModelService.cs </ProjectItem> <ProjectItem SubType="Code" TargetFileName="I$fileinputname$Service.cs" ReplaceParameters="true"> IModelService.cs </ProjectItem> The TargetFileName attribute is for the actual name of the file after executing of the template. In our case, if the user set “Employee” then the generated files will have following names: Employee.cs, IEmployeeRepository.cs, IEmployeeService.cs, EmployeeRepository.cs and EmployeeService.cs. In order to continue, we need to use a workaround, because using the $safeitemname$ token in multiple template files can present a challenge. (Read here for more info). Next, we will add a custom parameter in the vstemplate file. This will keep the correct value of the user’s input. Add the following code right after last tag: <CustomParameters> <CustomParameter Name="$basename$" Value="$fileinputname$"/> </CustomParameters> This is how we define a custom parameter in the Item/Project templates. Define Template Files IModelRepository: From the name, you may be able to guess this is the template file for the repository interface. Add the following code to this file: using System.Collections.Generic; using System.Threading.Tasks; namespace $rootnamespace$ { /// <summary> /// The $basename$ repository interface. /// </summary> public interface I$basename$Repository { /// <summary> /// Gets the element by identifier asynchronously. /// </summary> /// <param name="id">The identifier.</param> Task<$basename$> GetAsync(long id); /// <summary> /// Gets the all elements asynchronously. /// </summary> Task<IEnumerable<$basename$>> GetAllAsync(); } } ModelRepository: The implementation of the IModelRepository using System; using System.Collections.Generic; using System.Threading.Tasks; namespace $rootnamespace$ { /// <summary> /// The default <see cref="I$basename$Repository"/> implementation. /// </summary> public class $basename$Repository : I$basename$Repository { /// See <see cref="I$basename$Repository" /> for more. public Task<$basename$> GetAsync(long id) { throw new NotImplementedException(); } /// See <see cref="I$basename$Repository" /> for more. public Task<IEnumerable<$basename$>> GetAllAsync() { throw new NotImplementedException(); } } } IModelService: The interface of our service class using System.Collections.Generic; using System.Threading.Tasks; namespace $rootnamespace$ { /// <summary> /// The $basename$ service interface. /// </summary> public interface I$basename$Service { /// <summary> /// Gets the element by identifier asynchronously. /// </summary> /// <param name="id">The identifier.</param> Task<$basename$> GetAsync(long id); /// <summary> /// Gets the all elements asynchronously. /// </summary> Task<IEnumerable<$basename$>> GetAllAsync(); } } ModelService: The implementation of IModelService using System.Collections.Generic; using System.Threading.Tasks; namespace $rootnamespace$ { /// <summary> /// The default <see cref="I$basename$Service"/> implementation. /// </summary> public class $basename$Service : I$basename$Service { #region Fields private readonly I$basename$Repository _$basename$Repository; #endregion #region Constructors /// <summary> /// Initializes a new instance of the <see cref="$basename$Service"/> class. /// </summary> public $basename$Service(I$basename$Repository $basename$Repository) { _$basename$Repository = $basename$Repository; } #endregion #region Public Methods /// See <see cref="I$basename$Service" /> for more. public Task<$basename$> GetAsync(long id) { return _$basename$Repository.GetAsync(id); } /// See <see cref="I$basename$Service" /> for more. public Task<IEnumerable<$basename$>> GetAllAsync() { return _$basename$Repository.GetAllAsync(); } #endregion } } Now that we’ve completed our template files, we’re ready to deploy, use and share our item template. The solution explorer should look like this: Deploying a Custom Project or Item Template using the VSIX Project Now that we have built an item template, let’s look at how to deploy it. Add a new project to ModelEx solution. Select “Extensibility/VSIX Project,” and name it ModelExDeploy. The entire project is a single source.extension.vsixmanifest file. It provides metadata for the custom template we want to deploy. Use the designer to fill all necessary data you want. Right now, we just need to add our template as an asset to the VSIX package. Go to Assets tab, and click on New button. Select Microsoft.VisualStudio.ItemTemplate for Type. Pick a project in the current solution for Source, and select the ModelEx template project in Project field. Click OK, and close the designer. Set the ModelExDeploy project as a startup project, and hit F5. A new test instance of VS will start where your template is installed and ready to use. Open or create solution and from context menu. Select Add/New Item… There you can find our template ready for use. Pick a name. When generating is complete, you can see all five files are ready to use. Of course, when it comes to real projects, every file from these five are located in a different folder or even in a different project in the solution. The IWizard interface must be used during the template generation process. Using the IWizard Interface Add Custom Logic and UI to Customize the Template From the visual studio documentation: “The IWizard interface methods are called at various times while the template is being created, starting as soon as a user clicks OK on the New Item dialog box. Each method of the interface is named to describe the point at which it is called. For example, Visual Studio calls RunStarted immediately when it starts to create the item, making it a good location to write custom code to collect user input.” Let’s create a new class library project in our solution and call it ModelExWizard. In order to use the IWizard interface, we need to add references: EnvDTE EnvDTE80 Microsoft.VisualStudio.TemplateWizardInterface Microsoft.VisualStudio.Shell.14.0 System.Windows.Forms Next we must create a new class that implements the IWizard interface: public class TemplatePreProcessor : IWizard { public void BeforeOpeningFile(ProjectItem projectItem) { } public void ProjectFinishedGenerating(Project project) { } public void ProjectItemFinishedGenerating(ProjectItem projectItem) { } public void RunFinished() { } public void RunStarted( object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams) { } public bool ShouldAddProjectItem(string filePath) { return true; } } For now, we will move on and connect the wizard with our template. First, we must register our wizard assembly to GAC, and for that reason it is important to run VS as an administrator. Go to project properties, and sign the assembly: Add the following code in the Post-build event section "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\gacutil.exe" /if "$(TargetPath)" "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe" -T "$(TargetPath)" The paths can vary depending on your system. Now you can build the project. If correct, you will be shown this in the output window: After retrieving the public token, go to ModelEx.vstemplate file and add the following tag: Set the PublicKeyToken attribute with the token value generated in the build output window of the ModelExWizard project. Last, register the ModelExWizard assembly as an asset in the source.extension.vsixmanifest file, and include the debugging symbols in order to debug our code. To enable debugging, command F4 while ModelExDeploy project is selected, and change the following options to true: IncludeAssemblyInVSIXContainer IncludeDebugSymbolsInVSIXContainer IncludeDebugSymbolsInLocalVSIXDeployment Add the assembly as an asset through the source.extension.vsixmanifest designer. Go to the Assets tab, and click on New button, select: Type: Microsoft.VisualStudio.Assembly Source: A project in current solution Project: ModelExWizard Add Code to Your IWizard Implementation Class Now we are ready to add some real code in our IWizard implementation class. We will create a windows form control where the user will be prompted to choose a project from within the solution for each individual file. The end result will look like this: All projects are sent to the dropdowns via form’s constructor, and all projects are fetched via VS SDK. Our RunStarted method will look like this: _dte = (DTE)Package.GetGlobalService(typeof(DTE)); var projectList = new List<Project>(); foreach (Project project in _dte.Solution.Projects) { if (project.Kind == ProjectKinds.vsProjectKindSolutionFolder) { projectList.AddRange(GetSolutionFolderProjects(project)); } else { projectList.Add(project); } } OptionsForm form = new OptionsForm(projectList .Select(it => it.Name)); form.ShowDialog() After closing the form, we can fetch the selected project names picked by the user and set some custom template parameters that we will use later to construct the correct namespaces and usage in our template files. public void RunStarted( object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams) { ... form.ShowDialog() string _businessProject = form.SelectedBusinessProject; string _coreProject = form.SelectedCoreProject; string _dataProject = form.SelectedDataProject; string _modelsProject = form.SelectedModelsProject; replacementsDictionary.Add("$DomainNamespace$", _modelsProject + ".Domain")); replacementsDictionary.Add("$CoreRepoNamespace$", _coreProject + ".Repositories"); replacementsDictionary.Add("$CoreServiceNamespace$", _coreProject + ".Services"); replacementsDictionary.Add("$BuisnessNamespace$", _businessProject + ".Services"); replacementsDictionary.Add("$DataNamespace$", _dataProject + ".Repositories"); } Modifying for the ModelServices Template We can use the parameters we set above in all template files. This is how the ModelServices.cs template changed with the custom parameters: Last, we must move the already generated files to projects selected by the user. By default, templates are created where the user right clicks on the context menu in solution explorer and selects the Add New Item … option. We don’t want this. Instead, we will implement the ProjectItemFinishedGenerating(ProjectItem projectItem) method. This method is called for each of our template files. The projectItem object is the actual generated object which lives in the VS solution project. First we will identify which template the projectItem is using by examining the Name property of that object. Then, we will search through all our projects in the solution to find the one selected by the user. When we find the project, we use again the VS SDK to copy the contents of the projectItem object to the project. project.ProjectItems.AddFromFileCopy(projectItem.FileNames[0]); Because this is a copy operation, make sure to remove the source file from the solution. projectItem.Delete() The final version of this method also adds the files to folders. This will separate them from the other project files. For example, the repository is placed under “Repositories/” folder. The model is placed under “Domain/” folder, and the service goes under “Services/” folder. string itemName = Path.GetFileNameWithoutExtension(projectItem.Name); string folder = string.Empty; Project project = null; if (itemName == _modelName) { project = GetProjectByName(_modelsProject); folder = "Domain"; } else if (itemName == "I" + _modelName + "Service") { project = GetProjectByName(_coreProject); folder = "Services"; } else if (itemName == "I" + _modelName + "Repository") { project = GetProjectByName(_coreProject); folder = "Repositories"; } else if (itemName.EndsWith("Repository")) { project = GetProjectByName(_dataProject); folder = "Repositories"; } else if (itemName.EndsWith("Service")) { project = GetProjectByName(_businessProject); folder = "Services"; } project.AddToFolder(projectItem, folder); projectItem.Delete(); With a few clicks, the developer can create a fully functional domain, repository and service classes. The full source code of the project can be found here. Tags Development Share Share on Facebook Share on LinkedIn Share on Twitter Share Share on Facebook Share on LinkedIn Share on Twitter Sign up for our monthly newsletter. Sign up for our monthly newsletter.