This is a tutorial on how to create a project template for Visual Studio with multiple projects, commands, dialogs and external tools. The project template will have a mandatory and an optional project that you can choose from a dialog when the project is created. There will also be an item template that uses Text Template Transformation Toolkit (T4) to generate code from a DSL.
I have the following softwares installed on my machine.
- Visual Studio 2015 Update 1
- Visual Studio SDK
- Extensibility Tools for Visual Studio
- ILSyp
If you download the code from GitHub you have to change the debug settings for the VSIXProject to be able to debug.
- Start Action : C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe
- Command line arguments: /rootsuffix EXP
Change the debug settings for the VSIXProject when downloaded the code from GitHub
If you want to skip some steps in this tutorial, you can download the code and begin on the following steps
- Step 2 : Mandatory project template
- Step 3 : Optional project template
- Step 4 : NuGet packages
- Step 5 : Commands
- Step 6 : Create a custom item template
- Step 7 : Refactor some code to a reusable helper library
In the first step, we will create a VSIX Project, add a project template with a wizard (the SolutionWizard), where we by code could add other project templates to the solution.
First we need a solution. Open Visual Studio and follow the steps bellow.
Choose the blank solution project template
Now we have our solution that we can start adding project to.
The VSIX project where we will put all logic such as wizards, commands and dialogs.
Add a VSIX Project to the solution
Delete the unnecessary files
Add folders for Commands, Dialogs and Wizards
Add a project template whose sole purpose is to run a wizard where we can add the code to create our project. I add all my template projects to a solution folder called ProjectTemplates.
Add the solution project template
Delete the unnecessary files
<Project File="ProjectTemplate.csproj" ReplaceParameters="true">
<ProjectItem ReplaceParameters="true" TargetFileName="Properties\AssemblyInfo.cs">AssemblyInfo.cs</ProjectItem>
<ProjectItem ReplaceParameters="true" OpenInEditor="true">Class1.cs</ProjectItem>
</Project>
Delete the content of the TemplateContent element in the ProjectTemplateTutorial.Solution.vstemplate file
Change the type attribute of the VSTemplate element in the vstemplate file to ProjectGroup
Add a category for the project template in the new project dialog
Now we need to add a wizard class with the logic for creating our project template. Add a class to the Wizard folder in the VSIXProject, name it SolutionWizard. The SolutionWizard class should implement the IWizard interface. You also need to sign all projects in the solution.
Add the references to envdte and Microsoft.VisualStudio.TemplateWizardInterface in the VSIXProject
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EnvDTE;
namespace ProjectTemplateTutorial.VSIXProject.Wizards
{
public class SolutionWizard : IWizard
{
public void BeforeOpeningFile(ProjectItem projectItem)
{
}
public void ProjectFinishedGenerating(Project project)
{
}
public void ProjectItemFinishedGenerating(ProjectItem projectItem)
{
}
public void RunFinished()
{
int i = 0;
}
public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
{
}
public bool ShouldAddProjectItem(string filePath) => true;
}
}
The SolutionWizard class
Set a breakpoint in the int i = 0; line in the RunFinished method.
The only thing left to do is to add assets to the VSIXProject. Open the source.extension.vsixmanifest file in the VSIXProject. Add the assets below.
Add assembly
Add project template
I use ILSpy to get the strongname of the ProjectTemplate.VSIXProject.dll
<WizardExtension>
<Assembly>ProjectTemplateTutorial.VSIXProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b490d4518b7bc751</Assembly>
<FullClassName> ProjectTemplateTutorial.VSIXProject.Wizards.SolutionWizard</FullClassName>
</WizardExtension>
Add the WizardExtension element to VSTemplate element in the vstemplate file
Let´s try to create a project with our project template.
The project template is located in the tutorial category in the New Project dialog
The break point in the RunFinished method should be hit
An empty solution is created
We are done with the first step in this tutorial.
Let´s add the project template for the mandatory project. If you skiped the first step in this tutorial you can download the code from the Solution release and start the tutorial here.
Add the mandatory project template
We want to hide this project template in the New Project dialog and add the project to the solution from the SolutionWizard class.
<Hidden>true</Hidden>
Add the Hidden element to the TemplateData element in the ProjectTemplateTutorial.Mandatory.vstemplate file
Add the mandatory project template as an asset in the VSIXproject
Add the Microsoft.VisualStudio.Shell.14.0 NuGet package to the VSIXProject
Let's write the code to add the mandatory project to the solution. Visual Studio passes a dictionary with data from the New Project dialog to the wizard. We need to save this data and use it to create the mandatory project from our mandatory project template.
private Dictionary<string, string> _replacementsDictionary = new Dictionary<string, string>();
DTE _dte;
public SolutionWizard()
{
_dte = ServiceProvider.GlobalProvider.GetService(typeof(DTE)) as DTE;
}
Add a constructor and fields to the SolutionWizard class
public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
{
_replacementsDictionary = replacementsDictionary;
}
Store the replacementsDictionary in the field in the RunStarted method
public void RunFinished()
{
string destination = _replacementsDictionary["$destinationdirectory$"];
string fileName = _replacementsDictionary["$safeprojectname$"] + ".sln";
_dte.Solution.SaveAs(Path.Combine(destination, fileName));
var projectName = $"{_replacementsDictionary["$safeprojectname$"]}.Mandatory";
var templateName = "ProjectTemplateTutorial.Mandatory";
AddProject(destination, projectName, templateName);
}
Get the data for our project from the replacementsDictionary and pass it to the AddProject method
We will use the DTE object to add the project to our solution. Later we will refactor this method to a helper library so we can reuse it in other project templates but for now just put the AddProject method in the SolusionWizard class.
private void AddProject(string destination, string projectName, string templateName)
{
string projectPath = Path.Combine(destination, projectName);
string templatePath = ((Solution4)_dte.Solution).GetProjectTemplate(templateName, "cs");
_dte.Solution.AddFromTemplate(templatePath, projectPath, projectName, false);
}
Code to add the a project to the solution
Let's try the project template to see it the mandatory project is created.
The mandatory project is created
You can also add projects and use the build in project template if you like. You find the build in project templates in the C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\ProjectTemplates folder.
projectName = $"{_replacementsDictionary["$safeprojectname$"]}.WCFServiceLibrary";
AddProject(destination, projectName, "WcfServiceLibrary");
Add a WCFServiceLibrary to the solution
The project created with the built-in WCFServiceLibrary template is now added to the solution
We are done with step two of this tutorial.
Let´s add the project template and a dialog for the optional project to our project template. If you skiped the second step in this tutorial you can download the code from the Mandatory release and start the tutorial here.
Add the optional project template
<Hidden>true</Hidden>
Add the Hidden element to the TemplateData element in the ProjectTemplateTutorial.Mandatory.vstemplate file
Add the optional project template as an asset in the VSIXproject
We need a dialog to determine which projects should be created. The dialog will be implemented as a WPF window. Obviously, you can use the MVVM or other designs patterns you are comfortable with. We will stick to the simplest possible implementation of the dialog.
Add a reference to System.Xaml in the VSIXProject
Add a User Control (there is no window item temptale but we will change that)
We need to change the class from UserControl to a Window class. Visual Studio has a class called DialogWindow in the Microsoft.VisualStudio.PlatformUI namespace that we will use.
xmlns:platformUI="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.14.0"
Add the Microsoft.VisualStudio.PlatformUI namespace in the XAML file
In the XAML file change the root element from UserControl to platformUI:DialogWindow. In the code behind file change the base class from UserControl to DialogWindow.
We need some images to for our SolutionWizardDialog. Use the Image Export Moniker dialog from the extensibility tools to export images for Solution and CSProjectNode.
Create a new folder names Resources in the VSIXProject an add the files to that folder
Change the Build Action for the image files to Resource
<platformUI:DialogWindow x:Class="ProjectTemplateTutorial.VSIXProject.Dialogs.SolutionWizardDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ProjectTemplateTutorial.VSIXProject.Dialogs"
xmlns:platformUI="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.14.0"
Width="370"
Height="210"
Title="Create Solution"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ShowInTaskbar="False">
<Grid>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/Solution.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="10,10,0,0"/>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/CSProjectNode.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="33,31,0,0"/>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/CSProjectNode.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="33,51,0,0"/>
<TextBlock x:Name="SolutionNameTbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="30,10,10,0"
Text="SolutionName"/>
<TextBlock x:Name="MandatoryProjectNameTbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="53,31,17,0"
Text="MandatoryProjectName"/>
<CheckBox x:Name="OptionalProjectNameCbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="53,51,17,0"
IsChecked="True"
Content="OptionalProjectName" />
<Button x:Name="CancelBtn"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
Content="Cancel"
Margin="0,0,10,10"
Click="CancelBtn_Click"/>
<Button x:Name="OKBtn"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
Content="OK"
Margin="0,0,90,10"
Click="OKBtn_Click"/>
</Grid>
</platformUI:DialogWindow>
The XAML in the SolutionWizardDialog.xaml file
public partial class SolutionWizardDialog : DialogWindow
{
public SolutionWizardDialog(string safeProjectName)
{
InitializeComponent();
SolutionNameTbx.Text = $"{safeProjectName}";
MandatoryProjectNameTbx.Text = $"{safeProjectName}.Mandatory";
OptionalProjectNameCbx.Content = $"{safeProjectName}.Optional";
}
private void OKBtn_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void CancelBtn_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
The C# code in the code behing file
Let's open the dialog window when we create our project.
SolutionWizardDialog dialog = new SolutionWizardDialog(_replacementsDictionary["$safeprojectname$"]);
var result = dialog.ShowModal();
if (result == null || !result.Value)
{
throw new WizardCancelledException();
}
else
{
_addOptionalProject = (bool)dialog.OptionalProjectNameCbx.IsChecked;
}
Open the dialog in the RunStarted method of the SolutionWizard class and save the value of the checkbox in the _addOptionalProject field
if (_addOptionalProject)
{
projectName = $"{_replacementsDictionary["$safeprojectname$"]}.Optional";
templateName = "ProjectTemplateTutorial.Optional";
AddProject(destination, projectName, templateName);
}
Add code to the RunFinished method to add the optional project if the checkbox is checked
The SolutionWizardDialog pops up when you create a new project with the project template
If the checkbox for the optional project is checked the optional project will be created
We are done with step three of this tutorial.
To add NuGet packages to one or more of our projects we can do that by using the IVsPackageInstallerServices i Visual Studio. If you skiped the third step in this tutorial you can download the code from the Optional release and start the tutorial here.
Add the InstallNuGetPackage method to the SolutionWizard class. Later in this tutorial we will refactor this method and the AppProject method to a helper library so we can reuse it in other project templates but for now just put it in the SolusionWizard class.
Add a reference to Microsoft.VisualStudio.ComponentModelHost in the VSIXProject
Add the NuGet.VisualStudio NuGet package to the VSIXProject
private bool InstallNuGetPackage(string projectName, string package)
{
bool installedPkg = true;
Project project = (from Project p in (Array)_dte.ActiveSolutionProjects
where p.Name.Equals(projectName)
select p).First();
try
{
var componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel));
IVsPackageInstallerServices installerServices = componentModel.GetService<IVsPackageInstallerServices>();
if (!installerServices.IsPackageInstalled(project, package))
{
_dte.StatusBar.Text = @"Installing " + package + " NuGet package, this may take a minute...";
IVsPackageInstaller installer = componentModel.GetService<IVsPackageInstaller>();
installer.InstallPackage(null, project, package, (System.Version)null, false);
_dte.StatusBar.Text = @"Finished installing the " + package + " NuGet package";
}
}
catch (Exception ex)
{
string t = ex.Message;
installedPkg = false;
_dte.StatusBar.Text = @"Unable to install the " + package + " NuGet package";
}
return installedPkg;
}
Code that adds a NuGet package to a project
InstallNuGetPackage(projectName, "Newtonsoft.Json");
Install the Newtonsoft.Json NuGet package in the optional project
Newtonsoft.Json is added to the optional project
We are done with step four of this tutorial.
Commands are the way to implement a particular task in your project template. For example, the developer should be able to create a copyright note in every source code file in a project created with our project template. We will add a new context menu for the project node in the solution explorer. Then we will add some metadata to the project to be able to only show the command for the mandatory project. If you skiped the fourth step in this tutorial you can download the code from the NuGet release and start the tutorial here.
We will create the reusable command class RelayCommand that takes a delegate as a parameter for the eventhandlers. Later we will move this class to the helper library as well.
Add the RelayCommand to the Commands folder in the VSIXProject
Use the Auto-sync VSCT commands feature from the extensibility tools for the vsct file
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Commands package="guidRelayCommandPackage">
<Groups>
</Groups>
<Menus>
</Menus>
<Buttons>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages"
href="Resources\RelayCommand.png"
usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough"/>
</Bitmaps>
</Commands>
<CommandPlacements>
</CommandPlacements>
<Symbols>
<GuidSymbol name="guidRelayCommandPackage" value="{edc30286-8947-4257-9355-8d5d25829c5d}" />
<GuidSymbol name="guidRelayCommandPackageCmdSet" value="{977a44b1-3da7-4b57-9e13-253a15116874}">
</GuidSymbol>
<GuidSymbol name="guidImages" value="{9a4ae56f-11f2-443e-8533-e1c6a67b471d}" >
<IDSymbol name="bmpPic1" value="1" />
<IDSymbol name="bmpPic2" value="2" />
<IDSymbol name="bmpPicSearch" value="3" />
<IDSymbol name="bmpPicX" value="4" />
<IDSymbol name="bmpPicArrows" value="5" />
<IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>
</Symbols>
</CommandTable>
Clean up the vsct file and add the Menus and CommandPlacements elements
<GuidSymbol name="guidRelayCommandPackageCmdSet" value="{977a44b1-3da7-4b57-9e13-253a15116874}">
<IDSymbol name="ProjectContextGroup" value="0x0100" />
<IDSymbol name="ProjectContextMenu" value="0x0200" />
<IDSymbol name="ProjectContextMenuGroup" value="0x0300" />
<IDSymbol name="AddCopyrightCommand" value="0x0400"/>
</GuidSymbol>
Add the IDSymbols for the context menu and button
<Groups>
<Group guid="guidRelayCommandPackageCmdSet" id="ProjectContextGroup" priority="0x0000"/>
<Group guid="guidRelayCommandPackageCmdSet" id="ProjectContextMenuGroup" priority="0x0000"/>
</Groups>
Add the groups
<Menus>
<Menu guid="guidRelayCommandPackageCmdSet" id="ProjectContextMenu" type="Context" priority="0x0100">
<Strings>
<CommandName>Project Template Tutorial</CommandName>
<ButtonText>Project Template Tutorial</ButtonText>
<MenuText>Project Template Tutorial</MenuText>
<ToolTipText>Project Template Tutorial</ToolTipText>
</Strings>
</Menu>
</Menus>
Add the contextmenu
<Buttons>
<Button guid="guidRelayCommandPackageCmdSet" id="AddCopyrightCommand" priority="0x0100" type="Button">
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Add Copyright Comment</ButtonText>
</Strings>
</Button>
</Buttons>
Add the button
<CommandPlacements>
<CommandPlacement guid="guidRelayCommandPackageCmdSet" id="ProjectContextGroup" priority="0x0000">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_PROJNODE" />
</CommandPlacement>
<CommandPlacement guid="guidRelayCommandPackageCmdSet" id="ProjectContextMenu" priority="0x0100">
<Parent guid="guidRelayCommandPackageCmdSet" id="ProjectContextGroup" />
</CommandPlacement>
<CommandPlacement guid="guidRelayCommandPackageCmdSet" id="ProjectContextMenuGroup" priority="0x0100">
<Parent guid="guidRelayCommandPackageCmdSet" id="ProjectContextMenu" />
</CommandPlacement>
<CommandPlacement guid="guidRelayCommandPackageCmdSet" id="AddCopyrightCommand" priority="0x0100">
<Parent guid="guidRelayCommandPackageCmdSet" id="ProjectContextMenuGroup" />
</CommandPlacement>
</CommandPlacements>
Add the hirarcy of all symbols to place the button in the context menu and the context menu in the contextmenu for the project node in the solution explorer
internal sealed class RelayCommand
{
private readonly Package package;
public RelayCommand(Package package, int commandId, Guid commandSet, Action<object, EventArgs> menuCallback, Action<object, EventArgs> beforeQueryStatus = null)
{
this.package = package;
OleMenuCommandService commandService = ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if (commandService != null)
{
var MenuCommandID = new CommandID(commandSet, commandId);
var MenuItem = new OleMenuCommand(menuCallback.Invoke, MenuCommandID);
if (beforeQueryStatus != null)
{
MenuItem.BeforeQueryStatus += beforeQueryStatus.Invoke;
}
commandService.AddCommand(MenuItem);
}
}
private IServiceProvider ServiceProvider => this.package;
}
Implement the RelayCommand
[ProvideAutoLoad("{f1536ef8-92ec-443c-9ed7-fdadf150da82}")]
[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(PackageGuids.guidRelayCommandPackageString)]
[SuppressMessage("StyleCop.cs.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
public sealed class RelayCommandPackage : Package
{
private RelayCommand addCopyrightCommand;
public RelayCommandPackage()
{
}
protected override void Initialize()
{
addCopyrightCommand = new RelayCommand(this, PackageIds.AddCopyrightCommand, PackageGuids.guidRelayCommandPackageCmdSet,
(sender, e) =>
{
string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
string title = "RelayMenuCommandCallback";
// Show a message box to prove we were here
VsShellUtilities.ShowMessageBox(
ServiceProvider.GlobalProvider,
message,
title,
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
},
(sender, e) =>
{
var cmd = (OleMenuCommand)sender;
cmd.Visible = true;
});
base.Initialize();
}
}
Initialize the command in the package class. For now the command only shows a message box.
The command is located in the context menu of the project
MessageBox shown when the command is tiggered
protected override void Initialize()
{
addCopyrightCommand = new RelayCommand(
this,
PackageIds.AddCopyrightCommand,
PackageGuids.guidRelayCommandPackageCmdSet,
AddCopyrightComment
,
(sender, e) =>
{
var cmd = (OleMenuCommand)sender;
cmd.Visible = true;
});
base.Initialize();
}
Change the addCopyrightCommand object initialization
private void AddCopyrightComment(object sender, EventArgs e)
{
DTE dte = GetService(typeof(DTE)) as DTE;
Array projects = (Array)dte.ActiveSolutionProjects;
foreach (Project project in projects)
{
foreach (ProjectItem projectItem in project.ProjectItems)
{
Document document;
try
{
projectItem.Open();
document = projectItem.Document;
}
catch (Exception)
{
Console.WriteLine("failed to load document");
continue;
}
if (document == null)
{
continue;
}
TextDocument editDoc = (TextDocument)document.Object("TextDocument");
if (document.Name.EndsWith(".cs"))
{
EditPoint objEditPt = editDoc.CreateEditPoint();
objEditPt.StartOfDocument();
document.ReadOnly = false;
objEditPt.Insert("//-----------------------------------------------------------------------------");
objEditPt.Insert(Environment.NewLine);
objEditPt.Insert("// Copyright (c) The Corporation. All rights reserved.");
objEditPt.Insert(Environment.NewLine);
objEditPt.Insert("//-----------------------------------------------------------------------------");
objEditPt.Insert(Environment.NewLine);
document.Save(document.FullName);
}
}
}
}
Code to add the copyright text to every C# file in a project
Copyright comment added to code file
The Add Copyright Comment command is visible in all types of projects in Visual Studio. If the command is of a general nature, it is a desired behavior, but if the command is specific to this particular project template we want to hide the command if the project are not created with our project template.
The command is visible on all project types
Let's add some metadata to the project file for both of our projects and only show our command if the user right clicks on the mandatory project. We need two methods, one that adds the metadata to the project and one that checks if the project contains a specific value in the metadata.
Add a class named ProjectExtensions to the Commands folder in the VSIXProject, this class will also be moved to the helper library in the next step.
public static class ProjectExtensions
{
public static void SetResponsibility(this Project project, params ProjectResponsibilities[] responsibilities)
{
foreach (var res in Enum.GetValues(typeof(ProjectResponsibilities)))
{
string name = res.ToString();
project.Globals[name] = Boolean.FalseString;
project.Globals.set_VariablePersists(name, true);
}
foreach (var res in responsibilities)
{
string name = res.ToString();
project.Globals[name] = Boolean.TrueString;
project.Globals.set_VariablePersists(name, true);
}
}
public static bool IsProjectResponsible(this Project project, Enum responsibility)
{
if (project == null)
throw new ArgumentNullException(nameof(project));
if (project.Globals.get_VariableExists(responsibility.ToString()))
{
string propertyValue = (string)project.Globals[responsibility.ToString()];
bool propertyValueBoolean;
if (Boolean.TryParse(propertyValue, out propertyValueBoolean))
{
if (propertyValueBoolean)
{
return true;
}
}
}
return false;
}
}
Extension methods for the Project type to add and check for metadata
public enum ProjectResponsibilities
{
Mandatory,
Optional
}
We use an enum for the different metadata values
private Project AddProject(string destination, string projectName, string templateName)
{
string projectPath = Path.Combine(destination, projectName);
string templatePath = ((Solution4)_dte.Solution).GetProjectTemplate(templateName, "cs");
_dte.Solution.AddFromTemplate(templatePath, projectPath, projectName, false);
Project project = (from Project p in _dte.Solution.Projects
where p.Name.Equals(projectName)
select p).FirstOrDefault();
return project;
}
Modify the AddProject method in the SolutionWizard class. The AddFromTemplate always returns null thats why we need to interate through the project to find the newly created project and return it.
Project mandatoryPproject = AddProject(destination, projectName, templateName);
mandatoryPproject.SetResponsibility(ProjectResponsibilities.Mandatory);
Project optionalProject = AddProject(destination, projectName, templateName);
optionalProject.SetResponsibility(ProjectResponsibilities.Optional);
Set the responsibilities for the projects in the SolutionWizard class
<ProjectExtensions>
<VisualStudio>
<UserProperties Optional="False" Mandatory="True" />
</VisualStudio>
</ProjectExtensions>
Metadata in the csproj file for the mandatory project
<Buttons>
<Button guid="guidRelayCommandPackageCmdSet" id="AddCopyrightCommand" priority="0x0100" type="Button">
<Icon guid="guidImages" id="bmpPic1" />
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>TextChanges</CommandFlag>
<CommandFlag>DontCache</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
<Strings>
<ButtonText>Add Copyright Comment</ButtonText>
</Strings>
</Button>
</Buttons>
Add the CommandFlags elements to the Button element in the vsct file in the VSIXProject
addCopyrightCommand = new RelayCommand(
this,
PackageIds.AddCopyrightCommand,
PackageGuids.guidRelayCommandPackageCmdSet,
AddCopyrightComment,
(sender, e) =>
{
DTE dte = GetService(typeof(DTE)) as DTE;
Array projects = (Array)dte.ActiveSolutionProjects;
Project current = (Project)projects.GetValue(0);
var cmd = (OleMenuCommand)sender;
cmd.Visible = current.IsProjectResponsible(ProjectResponsibilities.Mandatory);
});
Change the command so that it only appears if the project has the mandatory responsibility
The Add Copyright Command shows up if you right click the mandatory project
The Add Copyright Command is hidden if you right click the optional project
The Add Copyright Command no longer shows up in other project templates
We are done with the command step of this tutorial.
Let's create an item template with a custom tool to generate C # code from Json data. If you skiped the fifth step in this tutorial you can download the code from the Commands release and start the tutorial here.
First we need the item template project.
Add an item template project to the ProjectTemplates folder
Add the item tempalte project to the assets in the VSIXProject
{
'Email': 'example@example.com',
'Active': true,
'CreatedDate': '2015-01-15T00:00:00Z'
}
Delete the Class.cs file in the item template project and a text file names Json.jc with the content above
<WizardExtension>
<Assembly>Microsoft.VSDesigner, Version=10.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a</Assembly>
<FullClassName>Microsoft.VSDesigner.ProjectWizard.ItemPropertyWizard</FullClassName>
</WizardExtension>
Add the WizardExtension element to the vstemplate file in the item template project
<DefaultName>Json.jc</DefaultName>
Change the default name to Json.jc
<TemplateContent>
<ProjectItem ReplaceParameters="true" TargetFileName="$safeitemname$.jc">Json.jc</ProjectItem>
</TemplateContent>
Change the TemplateContent element
Set the catergory property of the vstemplate file in the item tempalte project
The item template can be found in the Tutorial category in the New Item dialog
The Json1.jc file is created in the project
Now that we have a item template let's create a custom tool to generate a C# class from it.
Add a reference to Microsoft.VisualStudio.Designer.Interfaces in the VSIXProject
Add the Newtonsoft.Json NuGet package to the VSIXProject
Add a Runtime Text Tempalte to the Tools folder in the VSIXProject
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="Newtonsoft.Json" #>
<#@ parameter name="JsonText" type="System.String" #>
<#@ parameter name="JsonNamespace" type="System.String" #>
<#@ parameter name="JsonClass" type="System.String" #>
<#
Action<string, string> CreateProperty = null; // null to avoid compile-time error
CreateProperty = delegate(string name, string type)
{
#>
public <#= type #> <#= name #> { get; set; }
<#
};
#>
//------------------------------------------------------------------------------"
// <auto-generated>");
// This code was generated by a tool.");
// Runtime Version:4.0.30319.239");
//
// Changes to this file may cause incorrect behavior and will be lost if");
// the code is regenerated.");
// </auto-generated>");
//------------------------------------------------------------------------------"
using System;
using Newtonsoft.Json;
namespace <#= JsonNamespace #>
{
public partial class <#= JsonClass #>
{
<#
string propertyName = string.Empty;
JsonTextReader reader = new JsonTextReader(new StringReader(JsonText));
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
propertyName = (string)reader.Value;
break;
case JsonToken.String:
CreateProperty(propertyName, "string");
break;
case JsonToken.Boolean:
CreateProperty(propertyName, "bool");
break;
case JsonToken.Date:
CreateProperty(propertyName, "DateTime");
break;
default:
break;
}
}
#>
public string ToJson()
{
return JsonConvert.SerializeObject(this);
}
public static <#= JsonClass #> Create(string json)
{
return JsonConvert.DeserializeObject<<#= JsonClass #>>(json);
}
}
}
Code to generate a C# class from Json
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Designer.Interfaces;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
namespace ProjectTemplateTutorial.VSIXProject.Tools
{
[ComVisible(true)]
[Guid("1263af09-d434-4a54-8c86-4d4000c394ac")]
[ProvideObject(typeof(JsoncsFileGenerator))]
[CodeGeneratorRegistration(typeof(JsoncsFileGenerator), "JsoncsFileGenerator", "{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}", GeneratesDesignTimeSource = true)]
public class JsoncsFileGenerator : IVsSingleFileGenerator, IObjectWithSite
{
private object site = null;
private CodeDomProvider codeDomProvider = null;
private ServiceProvider serviceProvider = null;
private CodeDomProvider CodeProvider
{
get
{
if (codeDomProvider == null)
{
IVSMDCodeDomProvider provider = (IVSMDCodeDomProvider)SiteServiceProvider.GetService(typeof(IVSMDCodeDomProvider).GUID);
if (provider != null)
codeDomProvider = (CodeDomProvider)provider.CodeDomProvider;
}
return codeDomProvider;
}
}
private ServiceProvider SiteServiceProvider
{
get
{
if (serviceProvider == null)
{
IOleServiceProvider oleServiceProvider = site as IOleServiceProvider;
serviceProvider = new ServiceProvider(oleServiceProvider);
}
return serviceProvider;
}
}
public int DefaultExtension(out string pbstrDefaultExtension)
{
pbstrDefaultExtension = "." + CodeProvider.FileExtension;
return VSConstants.S_OK;
}
public int Generate(string wszInputFilePath, string bstrInputFileContents, string wszDefaultNamespace, IntPtr[] rgbOutputFileContents, out uint pcbOutput, IVsGeneratorProgress pGenerateProgress)
{
if (bstrInputFileContents == null)
throw new ArgumentException(bstrInputFileContents);
FileInfo fi = new FileInfo(wszInputFilePath);
string className = fi.Name.Split('.').First();
Dictionary<string, object> TemplateParameters = new Dictionary<string, object>();
TemplateParameters.Add("JsonText", bstrInputFileContents);
TemplateParameters.Add("JsonNamespace", wszDefaultNamespace);
TemplateParameters.Add("JsonClass", className);
JsonT4 t4 = new JsonT4();
t4.Session = TemplateParameters;
//t4.WriteLine("//------------------------------------------------------------------------------");
//t4.WriteLine("// <auto-generated>");
//t4.WriteLine("// This code was generated by a tool.");
//t4.WriteLine("// Runtime Version:4.0.30319.239");
//t4.WriteLine("//");
//t4.WriteLine("// Changes to this file may cause incorrect behavior and will be lost if");
//t4.WriteLine("// the code is regenerated.");
//t4.WriteLine("// </auto-generated>");
//t4.WriteLine("//------------------------------------------------------------------------------");
t4.Initialize();
string csCode = t4.TransformText();
//csCode = Format(csCode);
byte[] bytes = Encoding.UTF8.GetBytes(csCode);
if (bytes == null)
{
rgbOutputFileContents[0] = IntPtr.Zero;
pcbOutput = 0;
}
else
{
rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(bytes.Length);
Marshal.Copy(bytes, 0, rgbOutputFileContents[0], bytes.Length);
pcbOutput = (uint)bytes.Length;
}
return VSConstants.S_OK;
}
public void GetSite(ref Guid riid, out IntPtr ppvSite)
{
if (site == null)
Marshal.ThrowExceptionForHR(VSConstants.E_NOINTERFACE);
// Query for the interface using the site object initially passed to the generator
IntPtr punk = Marshal.GetIUnknownForObject(site);
int hr = Marshal.QueryInterface(punk, ref riid, out ppvSite);
Marshal.Release(punk);
Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
}
public void SetSite(object pUnkSite)
{
// Save away the site object for later use
site = pUnkSite;
// These are initialized on demand via our private CodeProvider and SiteServiceProvider properties
codeDomProvider = null;
serviceProvider = null;
}
}
}
Add the new class JsoncsFileGenerator to the tools folder in the VSIXProject
<ProjectItem ReplaceParameters="true" TargetFileName="$safeitemname$.jc" CustomTool="JsoncsFileGenerator">Json.jc</ProjectItem>
Add the CustomTool attribute to the ProjectItem element in the vstemplate file in the item template project
The item template now creates a generated C# file
//------------------------------------------------------------------------------"
// <auto-generated>");
// This code was generated by a tool.");
// Runtime Version:4.0.30319.239");
//
// Changes to this file may cause incorrect behavior and will be lost if");
// the code is regenerated.");
// </auto-generated>");
//------------------------------------------------------------------------------"
using System;
using Newtonsoft.Json;
namespace ProjectTemplateTutorial.Solution1.Optional
{
public partial class Json1
{
public string Email { get; set; }
public bool Active { get; set; }
public DateTime CreatedDate { get; set; }
public string ToJson()
{
return JsonConvert.SerializeObject(this);
}
public static Json1 Create(string json)
{
return JsonConvert.DeserializeObject<Json1>(json);
}
}
}
The generated code in the Json1.cs file
string templatePath = ((Solution4)_dte.Solution).GetProjectItemTemplate("ProjectTemplateTutorial.ItemTemplate", "cs");
optionalProject.ProjectItems.AddFromTemplate(templatePath, "Json1.jc");
Add the item template to the optional project in the SolutionWizard class
The item template is now added to the optional project by default when you create a new project
We are done with the item template step of this tutorial.
Let's clean up our code a bit. We have two methods, AddProject and InstallNuGetPackages in the SolutionWizard class, the RelayCommand class and the ProjectExtensions class that we could reuse in other project templates. It would also be nice to add the posibility to group project by adding them to solution folders. If you skiped the sixth step in this tutorial you can download the code from the ItemTemplate release and start the tutorial here.
We will implement our help library as extension methods for the Visual Studio API.
Start by adding a new class library project
Add the Microsoft.VisualStudio.Shell.14.0 NuGet package to the Helper project
Add the NuGet.VisualStudio NuGet package to the Helper project
Add references to the DTE in the Helper project
Add a reference to Microsoft.VisualStudio.ComponentModelHost in the Helper project
Add a reference to the helper library in the VSIXProject
public static class DteExtensions
{
}
Rename the Class1 file to DteExtensions and make it static
Projects are added to Solutions so let's make the Addproject method an extension method for the Solution class.
public static Project AddProject(this Solution solution, string destination, string projectName, string templateName)
{
string projectPath = Path.Combine(destination, projectName);
string templatePath = ((Solution4)solution).GetProjectTemplate(templateName, "cs");
solution.AddFromTemplate(templatePath, projectPath, projectName, false);
Project project = (from Project p in solution.Projects
where p.Name.Equals(projectName)
select p).FirstOrDefault();
return project;
}
Move the AddProject method to the DteExtensions class
We do not have a method for adding an item to a project so let's add one.
public static void AddItem(this Project project, string itemTemplateName, string itemName)
{
string templatePath = ((Solution4)project.DTE.Solution).GetProjectItemTemplate(itemTemplateName, "cs");
project.ProjectItems.AddFromTemplate(templatePath, itemName);
}
Add the new method AddItem to the DteExtensions class
NuGet packages are added to project so let's make the InstallNuGetPackage en axtension method on the Project class.
public static bool InstallNuGetPackage(this Project project, string packageName)
{
bool installedPkg = true;
try
{
var componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel));
IVsPackageInstallerServices installerServices = componentModel.GetService<IVsPackageInstallerServices>();
if (!installerServices.IsPackageInstalled(project, packageName))
{
IVsPackageInstaller installer = componentModel.GetService<IVsPackageInstaller>();
installer.InstallPackage(null, project, packageName, (System.Version)null, false);
}
}
catch (Exception ex)
{
installedPkg = false;
}
return installedPkg;
}
Move the InstallNuGetPackage to the DteExtensions class
Resonibilities are properties of a project so let's make it an extension method for the Project class. We don't want to hard code the responsibilities present in a project template so let's make the SetResponsibility method generic as well.
public static void SetResponsibility<T>(this Project project, params T[] responsibilities)
{
foreach (var res in Enum.GetValues(typeof(T)))
{
string name = res.ToString();
project.Globals[name] = Boolean.FalseString;
project.Globals.set_VariablePersists(name, true);
}
foreach (var res in responsibilities)
{
string name = res.ToString();
project.Globals[name] = Boolean.TrueString;
project.Globals.set_VariablePersists(name, true);
}
}
Move the SetResponsibility method to the DteExtensions class and make it a generic method
public static bool IsProjectResponsible(this Project project, Enum responsibility)
{
if (project == null)
throw new ArgumentNullException(nameof(project));
if (project.Globals.get_VariableExists(responsibility.ToString()))
{
string propertyValue = (string)project.Globals[responsibility.ToString()];
bool propertyValueBoolean;
if (Boolean.TryParse(propertyValue, out propertyValueBoolean))
{
if (propertyValueBoolean)
{
return true;
}
}
}
return false;
}
Move the IsProjectResponsible method to the DteExtensions class
Delete the ProjectExtensions class and rename the file to ProjectResponsibilities.
public enum ProjectResponsibilities
{
Mandatory,
Optional
}
Keep only the ProjectResponsibilities enum
Delete the AddProject and the InstallNuGetPackage methods in the SolutionWizard class.
public void RunFinished()
{
string destination = _replacementsDictionary["$destinationdirectory$"];
string fileName = _replacementsDictionary["$safeprojectname$"] + ".sln";
_dte.Solution.SaveAs(Path.Combine(destination, fileName));
var projectName = $"{_replacementsDictionary["$safeprojectname$"]}.Mandatory";
var templateName = "ProjectTemplateTutorial.Mandatory";
Project mandatoryPproject = _dte.Solution.AddProject(destination, projectName, templateName);
mandatoryPproject.SetResponsibility(ProjectResponsibilities.Mandatory);
if (_addOptionalProject)
{
projectName = $"{_replacementsDictionary["$safeprojectname$"]}.Optional";
templateName = "ProjectTemplateTutorial.Optional";
Project optionalProject = _dte.Solution.AddProject(destination, projectName, templateName);
optionalProject.SetResponsibility(ProjectResponsibilities.Optional);
optionalProject.InstallNuGetPackage("Newtonsoft.Json");
optionalProject.AddItem("ProjectTemplateTutorial.ItemTemplate", "Json1.jc");
}
}
Use the new extension methods in the RunFinished method in the SolutionWizard class
using ProjectTemplateTutorial.Helpers;
Add the using for the helper library to the RelayCommandPackage class
To be able to group projects that have the same functionality in our solution it may be helpful to add projects to solution folders. Let's add the functionality to handle solution folder in our project template.
We want to add solutionfolders but first we need to check if they already exists.
public static SolutionFolder GetSolutionFolderEx(this Solution solution, string folderName)
{
Project solutionFolder = (from p in ((Solution2)solution).Projects.OfType<Project>()
where p.Name.Equals(folderName)
select p).FirstOrDefault();
return solutionFolder?.Object;
}
public static SolutionFolder GetSolutionFolderEx(this SolutionFolder solutionFolder, string folderName)
{
ProjectItem folder = (from p in solutionFolder.Parent.ProjectItems.OfType<ProjectItem>()
where p.Name.Equals(folderName)
select p).FirstOrDefault();
return ((Project)folder?.Object)?.Object;
}
The GetSolutionFolderEx methods return the SolutionFolder object if it exists otherwise they return null
Now we are ready to add solution folders to the project template. We need a way to add a solution folder to the root of the solution and a way to add a solution folder to another solutionfolder.
public static SolutionFolder AddSolutionFolderEx(this Solution solution, string folderName)
{
SolutionFolder folder = solution.GetSolutionFolderEx(folderName);
if (folder == null)
{
folder = ((Solution4)solution).AddSolutionFolder(folderName).Object;
}
return folder;
}
public static SolutionFolder AddSolutionFolderEx(this SolutionFolder solutionFolder, string folderName)
{
SolutionFolder folder = solutionFolder.GetSolutionFolderEx(folderName);
if (folder == null)
{
folder = solutionFolder.AddSolutionFolder(folderName).Object;
}
return folder;
}
The AddSolutionFolderEx methods adds a folder to either the solution or another SolutionFolder and returns the newly created SolutionFolder. If the SolutionFolder already exists it will return that SolutionFolder.
Because the method AddFromTemplate always returns null, we need a way to find the newly created project. Create a new method that we can reuse when we add a project to our solution or a solution folder.
public static Project GetProject(this Solution solution, string projectName) =>
solution.GetAllProjects().Where(p => p.Name.Equals(projectName)).FirstOrDefault();
private static IEnumerable<Project> GetAllProjects(this Solution solution)
{
return solution.Projects
.Cast<Project>()
.SelectMany(GetChildProjects)
.Union(solution.Projects.Cast<Project>())
.Where(p => { try { return !string.IsNullOrEmpty(p.FullName); } catch { return false; } });
}
private static IEnumerable<Project> GetChildProjects(Project parent)
{
try
{
if (parent.Kind != ProjectKinds.vsProjectKindSolutionFolder && parent.Collection == null) // Unloaded
return Enumerable.Empty<Project>();
if (!string.IsNullOrEmpty(parent.FullName))
return new[] { parent };
}
catch (COMException)
{
return Enumerable.Empty<Project>();
}
return parent.ProjectItems
.Cast<ProjectItem>()
.Where(p => p.SubProject != null)
.SelectMany(p => GetChildProjects(p.SubProject));
}
Get project by name
public static Project AddProject(this Solution solution, string destination, string projectName, string templateName)
{
string projectPath = Path.Combine(destination, projectName);
string templatePath = ((Solution4)solution).GetProjectTemplate(templateName, "CSharp");
solution.AddFromTemplate(templatePath, projectPath, projectName, false);
return solution.GetProject(projectName);
}
public static Project AddProject(this SolutionFolder solutionFolder, string destination, string projectName, string templateName)
{
string projectPath = Path.Combine(destination, projectName);
string templatePath = ((Solution4)solutionFolder.DTE.Solution).GetProjectTemplate(templateName, "CSharp");
solutionFolder.AddFromTemplate(templatePath, projectPath, projectName);
return solutionFolder.DTE.Solution.GetProject(projectName);
}
Use the GetProject method in the already existing AddProject method. Add a method to add a project to a solution folder
Export a new image for the folders and add the file to the Resources folder in the VSIXProject
Change the the Build Action of the FolderOpened.png file to Recource
<platformUI:DialogWindow x:Class="ProjectTemplateTutorial.VSIXProject.Dialogs.SolutionWizardDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ProjectTemplateTutorial.VSIXProject.Dialogs"
xmlns:platformUI="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.14.0"
Width="370"
Height="210"
Title="Create Solution"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ShowInTaskbar="False">
<Grid>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/Solution.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="10,10,0,0"/>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/FolderOpened.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="30,31,0,0"/>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/FolderOpened.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="50,52,0,0"/>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/CSProjectNode.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="70,73,0,0"/>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/FolderOpened.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="50,94,0,0"/>
<Image Source="pack://application:,,,/ProjectTemplateTutorial.VSIXProject;component/Resources/CSProjectNode.png"
Width="15"
Height="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="70,114,0,0"/>
<TextBlock x:Name="SolutionNameTbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="30,10,10,0"
Text="SolutionName"/>
<CheckBox x:Name="SourceFolderCbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Content="Source"
Margin="50,31,10,0" />
<CheckBox x:Name="MandatoryProjectSolutionFolderCbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="70,52,10,0"
IsChecked="True"
Content="Mandatory"/>
<TextBlock x:Name="MandatoryProjectNameTbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="90,73,10,0"
Text="MandatoryProjectName"/>
<CheckBox x:Name="OptionalProjectSolutionFolderCbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="70,94,10,0"
IsChecked="True"
Content="Optional"/>
<CheckBox x:Name="OptionalProjectNameCbx"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="90,114,10,0"
IsChecked="True"
Content="OptionalProjectName" />
<Button x:Name="CancelBtn"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
Content="Cancel"
Margin="0,0,10,10"
Click="CancelBtn_Click"/>
<Button x:Name="OKBtn"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="75"
Content="OK"
Margin="0,0,90,10"
Click="OKBtn_Click"/>
</Grid>
</platformUI:DialogWindow>
Add icons and checkboxes for the solution folder in the SolutionWizardDialog.xaml file
private bool _sourceFolder;
private bool _mandatoryFolder;
private bool _optionalFolder;
Add new fileds to the SolutionWizard class to store values from the SolutionWizardDialog
_sourceFolder = (bool)dialog.SourceFolderCbx.IsChecked;
_mandatoryFolder = (bool)dialog.MandatoryProjectSolutionFolderCbx.IsChecked;
_optionalFolder = (bool)dialog.OptionalProjectSolutionFolderCbx.IsChecked;
Store the values from the SolutionWizardDialog in the new fileds in the RunStarted method in the SolutionWizard class
private Project AddProject(string projectSufix, string templateName, SolutionFolder sourceSolutionFolder = null, string folderName = null)
{
string destination = _replacementsDictionary["$destinationdirectory$"];
if (_sourceFolder)
{
destination = Path.Combine(destination, "Source");
}
var projectName = $"{_replacementsDictionary["$safeprojectname$"]}.{projectSufix}";
Project project;
if (sourceSolutionFolder == null)
{
if (folderName != null)
{
SolutionFolder optionalFolder = _dte.Solution.AddSolutionFolderEx(folderName);
project = optionalFolder.AddProject(destination, projectName, templateName);
}
else
{
project = _dte.Solution.AddProject(destination, projectName, templateName);
}
}
else
{
if (folderName != null)
{
SolutionFolder folder = (SolutionFolder)sourceSolutionFolder.AddSolutionFolderEx(folderName);
project = folder.AddProject(destination, projectName, templateName);
}
else
{
project = sourceSolutionFolder.AddProject(destination, projectName, templateName);
}
}
return project;
}
Add a new private method called AddProject that handles the project creation logic in the SolutionWizard class
public void RunFinished()
{
string destination = _replacementsDictionary["$destinationdirectory$"];
string fileName = _replacementsDictionary["$safeprojectname$"] + ".sln";
_dte.Solution.SaveAs(Path.Combine(destination, fileName));
SolutionFolder sourceSolutionFolder = null;
if (_sourceFolder)
sourceSolutionFolder = _dte.Solution.AddSolutionFolderEx("Source");
string mandatoryFolderName = null;
if (_mandatoryFolder)
mandatoryFolderName = "Mandatory";
string optionalFolderName = null;
if (_optionalFolder)
optionalFolderName = "Optional";
Project mandatoryPproject = AddProject("Mandatory", "ProjectTemplateTutorial.Mandatory", sourceSolutionFolder, mandatoryFolderName);
mandatoryPproject.SetResponsibility(ProjectResponsibilities.Mandatory);
Project mandatoryPproject2 = AddProject("Mandatory2", "ProjectTemplateTutorial.Mandatory", sourceSolutionFolder, mandatoryFolderName);
mandatoryPproject2.SetResponsibility(ProjectResponsibilities.Mandatory);
if (_addOptionalProject)
{
Project optionalProject = AddProject("Optional", "ProjectTemplateTutorial.Optional", sourceSolutionFolder, optionalFolderName);
optionalProject.SetResponsibility(ProjectResponsibilities.Optional);
optionalProject.InstallNuGetPackage("Newtonsoft.Json");
optionalProject.AddItem("ProjectTemplateTutorial.ItemTemplate", "Json1.jc");
}
}
Change the RunFinished method in the SolutionWizard class so that it uses the new AddProject method
Add a reference to VSLangProj in the Helpers project
public static void AddReference(this Project project, Project projectToAdd)
{
(project.Object as VSProject).References.AddProject(projectToAdd);
}
Add the AddReference method to the DteExtensions class
mandatoryPproject.AddReference(optionalProject);
Add the optional project as a reference to the mandatory project in the SolutionWizzard class
public static void SetAsStartup(this Project project)
{
DTE _dte = ServiceProvider.GlobalProvider.GetService(typeof(DTE)) as DTE;
_dte.Solution.Properties.Item("StartupProject").Value = project.Name;
}
Add the SetAsStartup method to the DteExtensions class
optionalProject.SetAsStartup();
Set the optional project as the startup project in the SolutionWizard class
If an operation is taking longer then a second it is nice to give the user some feedback about what is happening. In Visual Studio we can do that in the statusbar.
public static void ShowStatusBarMessage(this string message)
{
DTE _dte = ServiceProvider.GlobalProvider.GetService(typeof(DTE)) as DTE;
_dte.StatusBar.Text = message;
}
Add the ShowStatusBarMessage method to the DteExtensions class
$"Installing {packageName} NuGet package, this may take a minute...".ShowStatusBarMessage();
Show a message in the statusbar when a nuget package is being installed
Visual Studio telling the user that the Newtonsoft.Json NuGet package is added
Add a reference to System.Design in the Helpers project
Add a references to Microsoft.Shell.Interop.{10.0, 11.0, 12.0} in the Helpers project
public class RelayCommand
{
private readonly Package package;
public RelayCommand(Package package, int commandId, Guid commandSet, Action<object, EventArgs> menuCallback, Action<object, EventArgs> beforeQueryStatus = null)
{
this.package = package;
OleMenuCommandService commandService = ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if (commandService != null)
{
var MenuCommandID = new CommandID(commandSet, commandId);
var MenuItem = new OleMenuCommand(menuCallback.Invoke, MenuCommandID);
if (beforeQueryStatus != null)
{
MenuItem.BeforeQueryStatus += beforeQueryStatus.Invoke;
}
commandService.AddCommand(MenuItem);
}
}
private IServiceProvider ServiceProvider => this.package;
}
Move the RelayCommand class to the Helper project
You now completed the tutorial on how to create a project template with multiple projects, item templates with a custom tool, dialogs and learned how to implement and show a command in the menus of Visual Studio. I hope you find this tutorial usefull. If you want to get the final code just clone the repository.