Updated Visual Studio Templates for VS 2017
Now that VS 2017 is ready it is time to update the template extensions I provided for previous Visual Studio versions. However this time the templates themselves don’t really need to change. Instead the VS extension that I wrote to wrap them needs to be updated to take advantage of the newer VS extension features.
Side by Side Visual Studio Instances
One of the biggest changes in VS 2017 is the ability to have multiple editions of the same VS version on the same machine at the same time (side by side). This means that an extension can no longer rely on global VS information. Instead the extension must use the information provided by the version it is installed to. This also means that extensions should not be installed globally, or if they are, then they should be versioned accordingly.
This impacts MSBuild as well. In previous versions MSBuild was installed to %ProgramFiles(x86)%\MSBuild
. Starting with VS2017 the MSBuild files are located under the VS instance that is going to trigger the build. Since there can be multiple instances installed, an extension must register any build changes (such as custom targets files) with the instance it is installed with. Fortunately getting to the MSBuild folder has been wrapped by a pseudo build variable for a while called MSBuildExtensionsPath (or MSBuildExtensionsPath32). This variable has been updated in VS2017 to point to the per-instance version so any code relying on this variable needs to ensure that the correct path was updated. We will need to make this change later on so that we can properly support projects that used the older version of our extension.
Extension Registry Files
Another important change is that VS is moving away from a central registry location. Instead each VS instance is storing most of its data in a unique registry key. This makes finding/updating VS information in the registry difficult at best. Extensions no longer update the VS registry. Instead each extension gets its own registry data file. Using the cool feature of registry files, VS will dynamically load the extensions registry information from the file but it will appear as though it is stored in the registry. This makes debugging harder unless you know where to look.
A registry data file is a binary file. It will generally be at the root of the extension’s folder (default is %VSInstallPath%\Common7\IDE\Extensions\{UniqueId}
). Using RegEdit, select the HKEY_USERS
key and then use File\Load Hive
to open the file. It will be loaded into the key and you can see the registry entries that would be merged into the core VS entries. This is useful for debugging issues with the registry.
Updating the Extension Project
The first thing we need to do is update the projects to VS 2017. Some projects need to be (one-way) upgraded while others do not. In addition all the projects need to be updated to use .NET 4.6 or higher.
The only other change is to update the VS references to the new version. This can be done by installing the VS 2017 SDK and then updating the projects to use the new references as has been done in the past, but there is a better way.
Updating Dependencies
Starting with VS 2017 the bulk of the SDK is now available as NuGet packages. Since most companies prefer their dependencies via NuGet this is the best way to go. Note that you can continue to use the SDK assemblies if you like but relying on NuGet will allow for automated builds without the need for the SDK. For the most part you simply need to remove each of the original assembly references and replace them with the NuGet package of the corresponding name.
- EnvDTE
- TemplateWizards
- TextTemplating
- EnvDTE80
- TextTemplating
- Microsoft.VisualStudio.CoreUtility
- ItemTemplates
- TextTemplates
- Microsoft.VisualStudio.OLE.Interop
- TextTemplating
- Microsoft.VisualStudio.Shell.14.0 –> Microsoft.VisualStudio.Shell.15.0
- TextTemplating
- Microsoft.VisualStudio.Shell.Design
- TextTemplating
- Microsoft.VisualStudio.Shell.Interop
- TextTemplating
- Microsoft.VisualStudio.TemplateWizardInterface –> VSSDK.TemplateWizardInterface
- TemplateWizards
- Microsoft.VisualStudio.TextTemplating.14.0 –> Microsoft.VisualStudio.TextTemplating.15.0
- TextTemplating
- Microsoft.VisualStudio.TextTemplating.Interfaces.10.0
- TextTemplating
- Microsoft.VisualStudio.TextTemplating.Interfaces.14.0
- TextTemplating
- VSLangProj
- TextTemplating
As part of this change we’ll go ahead and update to storing the NuGet packages in the project files instead of a packages.config
. One restriction on using the NuGet approach is that you will still need to have the SDK installed if you intend to work with the project designers. The designers are not part of NuGet.
The only other dependency we have is with T4 Toolbox.Unfortunately at the time of this writing the extension had not been updated to VS 2017. So, like in previous versions, I grabbed the source code, made the necessary changes and built the updated extension. The binary itself is needed by our extension so I copied the build output to the Assemblies
folder and rebuilt.
Updating the VSIX Project
Opening the .vsixmanifest
file we need to make a few changes. The first set of changes are around naming and versioning. This extension only works for VS2017 so I’m going to update the name to specify it is for VS 2017. Since this extension may be in the same gallery as the previous version I also update the ID to specify VS2017 instead of VS2015. Finally, I update the version number of the code and the manifest. The code uses the AssemblyVersion.cs file in the solution root. To make it easier to associate versions with VS versions I’m going to reset the version number to 15.0.0 (to match VS).
- Product Name = P3Net Text Templates (VS 2017)
- Product ID = P3NetTemplates.VS2017
- Version = 15.0.0
Since we modified the version of the assemblies we have to go to the item template for the environmental config template (EnvConfigs.vstemplate
) and change the version number in the WizardExtension
element to match.
Specifying Dependencies
The VSIX project schema has been updated to v3 so attempting to open this project will trigger a migration. This was necessary because of the changes made to the VS installer. Having to install large portions of VS just to use a few of the features is gone. Users can now pick and choose the components (or workloads) they want to use. Because of this extensions now need to identify the components they require to work correctly. It is no longer sufficient to simply specify the version and edition of VS. Microsoft has published the list of components currently installable. Each extension needs to identify the component(s) that it needs and add a dependency on them. At a minimum most extensions will require the Visual Studio core editor
which is basically the VS shell.
Under the Install Targets
section we need to update to specify that we require VS 2017. The format has changed a little bit so we’ll just remove the original target and then add in a target of Microsoft.VisualStudio.Community
. The version specified will be for VS 2017.
Under the Dependencies
section we are going to remove the dependency on T4 Toolbox. When this extension is updated later we can always add a dependency back but for now it eliminates the need for updating the extension when it is released.
Under the Prerequisites
section is where we need to list the components our extension relies on. Since our component has an environmental transform and some C# code generation templates we’ll need to add the following.
- Visual Studio core editor – the shell
- Managed Desktop Workload Core – the managed compilers
- C# and Visual Basic Roslyn compilers – not needed right now but prepping for the future
In all cases the version range will be for whatever is installed on your machine. I have modified the minimal versions to allow anything from 15.0 on.
Using the Experimental Instance
At this point we are ready to build and debug our extension. In previous releases I haven’t used the experimental instance of VS for several reasons but in VS 2017 I’m committed to using it. This allows for easier debugging and prevents corruption of my primary environment. So make sure that the options to deploy to the experimental instance are turned on in the VSIX project. Also mark this project as the startup project. Be sure to set the start action as follows.
- Start external program = path to DevEnv.exe
- Command line arguments = /RootSuffix Exp
Compile and run the code. VS should start up and you should see your extension installed. The extension has a dependency on T4 Toolbox so you need to go ahead and install it in the experimental instance. Unfortunately the UI does not support loading extensions from files directly and double clicking the VSIX will install it in the normal instance. There is a command line tool called vsixinstaller
(use the developer command prompt) that allows you to install/uninstall extensions. By default it works with all instances although you can specify an instance ID to target a specific instance. Unfortunately it doesn’t work with the experimental instance, at least I haven’t gotten it to. The only real workaround I’ve found, until it shows up on the marketplace, is to build the source so it auto-registers with the experimental instance.
Creating a Build Tasks Project
Previously we had used an inline build task to transform the environmental configs. This works but, as mentioned then, had issues with how the inline task was loaded by MSBuild. We’re going to be updating how we install the targets file so now is a good time to go ahead and move the inline build task to an assembly that we can simply reference in the targets file. This should speed things up and eliminate the issues we had in the past. We won’t change the behavior or inputs to the task, just move it to its own assembly.
- Create a new class library called
P3Net.BuildExtensions
. Ensure it targets .NET 4.6 or higher. - Add NuGet references to the following packages.
- Microsoft.Build.Framework
- Microsoft.Build.Utilities.Core
- Add a binary reference to
Microsoft.Web.Publishing.Tasks.dll
found under the MSBuild folder of VS2017. - Create a new class called
TransformXmlFiles
. - The code basically copies across straight from the targets file.
public class TransformXmlFiles : Task { [Required] public string SourceFile { get; set; } [Required] public string TargetFile { get; set; } [Required] public ITaskItem[] TransformFiles { get; set; } [Required] public string OutputDirectory { get; set; } public string ProjectName { get; set; } public string ToolsDirectory { get; set; } public override bool Execute () { Log.LogMessage("Begin TransformXmlFiles"); if (TransformFiles?.Any() ?? false) { Log.LogMessage("Creating Microsoft.Web.Publishing.Tasks.TransformXml"); var transform = new Microsoft.Web.Publishing.Tasks.TransformXml() { BuildEngine = BuildEngine, HostObject = HostObject }; foreach (var inputFile in TransformFiles) { Log.LogMessage($"Preparing to transform '{inputFile.ItemSpec}'"); var fileParts = Path.GetFileNameWithoutExtension(inputFile.ItemSpec).Split('.'); var envName = fileParts.LastOrDefault(); var outDir = Path.Combine(OutputDirectory, envName); if (!String.IsNullOrEmpty(ProjectName)) outDir = Path.Combine(outDir, ProjectName); var outFile = Path.Combine(outDir, TargetFile); if (!Directory.Exists(outDir)) { Log.LogMessage($"Creating directory '{outDir}'"); Directory.CreateDirectory(outDir); }; transform.Destination = outFile; transform.Source = SourceFile; transform.Transform = inputFile.ItemSpec; Log.LogMessage($"Transforming file"); if (!transform.Execute()) { Log.LogError($"Error transforming file"); return false; }; }; }; Log.LogMessage("End TransformXmlFiles"); return true; } }
Now we need to update the .targets file to use the new assembly and task. But to do that we’ll need to set up a new VSIX project.
Installing Targets Files
In VS 2017, as mentioned earlier, MSBuild is now per-VS instance so we need to copy it there instead. This could be an issue for users so VSIXs can now install targets files as well. Installing a targets file still requires admin privileges since it is writing to the shared VS instance but can otherwise be installed like any other extension now. The file is also going to be needed on build servers so I want to separate the targets file from the rest of the extension so we’ll create a new VSIX project just for this file. Since our original extension needs this file we’ll go ahead and add a dependency to the new extension in the original extension as well.
- Create a new VSIX project in the solution called
P3Net.BuildExtensions.Setup
. - Set up the manifest as was done earlier.
- For install targets we will target VS 2017 Community.
- Ensure that the VSIX is marked as install for all users.
- For assets select the
P3Net.BuildExtensions
project we just created. - For dependencies we only need the standard .NET framework.
- For prerequisites we need the following
- Visual Studio core editor
- MSBuild
- ASP.NET and we development tools
To install targets files we need to point them to the MSBuild folder. In the past we simply dropped the files into the root but we want to start versioning them with MSBuild so we’ll create a root folder for our targets and then use version-subfolders to support each version of MSBuild we’ll use. To allow older projects to work we’ll also create a root targets file that points to the current version-specific version.
- Add a new
Targets
folder to the VSIX project. - Copy the existing targets file into each folder.
- Edit the properties for the file
- Set Build Action to Content
- Set Include in VSIX to True
- Set Install Root to MSBuild
- Set VSIX Sub Path to P3Net
The root file should contain the following to redirect to the current version.
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath32)\P3Net\$(VisualStudioVersion)\P3Net.targets" /> </Project>
- Add a new
15.0
folder under the previous folder. - Copy the existing targets file into each folder.
- Edit the properties for the file
- Set Build Action to Content
- Set Include in VSIX to True
- Set Install Root to MSBuild
- Set VSIX Sub Path to P3Net\15.0
The new file should contain the following code to use the new build task.
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <UsingTask TaskName="TransformXmlFiles" AssemblyFile="P3Net.BuildExtensions.dll" /> <!-- Get the config transform files --> <ItemGroup> <WebConfigTransformFiles Include="web.*.config" Exclude="web.config" /> <AppConfigTransformFiles Include="app.*.config" Exclude="app.config" /> </ItemGroup> <!-- Runs after a successful build --> <Target Name="TransformConfigurationFiles" AfterTargets="AfterBuild"> <TransformXmlFiles TransformFiles="@(WebConfigTransformFiles)" SourceFile="web.config" TargetFile="web.config" OutputDirectory="$([System.IO.Path]::Combine($(OutDir), 'Configs'))" ProjectName="$(MSBuildProjectName)" Condition="Exists('web.config')" /> <TransformXmlFiles TransformFiles="@(AppConfigTransformFiles)" SourceFile="app.config" TargetFile="$(TargetFileName).config" OutputDirectory="$([System.IO.Path]::Combine($(OutDir), 'Configs'))" ProjectName="$(MSBuildProjectName)" Condition="Exists('app.config')" /> </Target> </Project>
In order for the targets file to find the build extensions they need to be in the same directory so we need to adjust the VSIX properties for the reference as well.
- Set Install Root to MSBuild
- Set VSIX Sub Path to P3Net\15.0
Finally, we need to update the environment transform wizard to reference the versioned targets file.
private void EnsureStandardTargetIsImported ( EnvDTE.Project project ) { var buildProject = ProjectCollection.GlobalProjectCollection.GetLoadedProjects(project.FullName).First(); //Check for the import var hasImport = (from i in buildProject.Xml.Imports where String.Compare(Path.GetFileName(i.Project), SharedTargetsFileName, true) == 0 select i).Any(); if (hasImport) return; //Make sure it exists first var extensionsPath = buildProject.GetPropertyValue("MSBuildExtensionsPath"); var version = buildProject.GetPropertyValue("MSBuildToolsVersion"); if (String.IsNullOrEmpty(version)) version = "15.0"; var targetsPath = $@"{SharedTargetsFilePath}\{version}\{SharedTargetsFileName}"; var fullPath = Path.Combine(extensionsPath, targetsPath); if (!File.Exists(fullPath)) ReportErrorAndCancel("The standard .targets file could not be located."); //Add it buildProject.Xml.AddImport($@"$(MSBuildExtensionsPath)\{targetsPath}"); }
Almost done. Now just add the new extension as a dependency on the original extension and rebuild.
There is one issue to be aware of with the deployment via VS. It will not install things to the MSBuild folder because it is not running as an administrator. Therefore you need to manually copy the targets folder and assemblies to the appropriate location before running. This only needs to happen the first time you create the targets, unless you change them. When running using the VSIX installer the files should be copied correctly.
Final Thoughts
We’re done. We have updated the template extension to VS 2017. Overall it was a minor upgrade but we made a lot of changes to make it easier to maintain down the road. Hopefully the next release will be even easier to upgrade.
Download the code.