Tuesday, October 10, 2017

Bundle your Visual Studio Solution as a Multi-Project Template

Earlier this year I provided a walkthrough of setting up a Xamarin.Forms project that leveraged Caliburn.Micro for Android, iOS and UWP. I had big plans for extracting the contents of that walkthrough and providing it as a NuGet package. Plans changed however, and I’ve decided to package the entire solution as a Multi-Project Template and provide it as an add-on to Visual Studio (VSIX). This post introduces provides a walk-through on how to create multi-project templates.

Wait, why not NuGet?

First off, as an aside, let’s go back and look what I wanted to do. I wanted to provide a starter-kit of files that would jump start your efforts and allow you to modify my provided files as you see fit. As a NuGet package, I can deliver these files to any project simply by adding these loose code files in the content folder of the NuGet package. Two things that are really awesome about this: the code files can be treated as source code transforms by changing their extension to *.pp, and through platform targeting I could deliver different content files per platform (Xamarin.iOS10, Xamarin.Android10, uap10.0, etc). With this approach, you would simply create a new Xamarin.Forms project then add the NuGet package to all projects. Bam. Easy.

But there are a few problems with this approach:

  • Existing files. My NuGet package would certainly be replacing existing files in your solution. I’d want to overwrite key parts of the initial template (App.xaml, AppDelegate, Activity, etc) and in some cases delete files (MainPage.xaml). Technically, I can overcome these side-effects by modifying the project through a NuGet install script (install.ps1). However, you would be prompted during the install about the replacements and if you clicked ‘No’ when prompted to replace these files… my template wouldn’t work.
  • Delivering Updates. This is the funny thing about this approach -- it is really intended as a one time deal. You would add the starter files to your project and then begin to modify and extend to your hearts’ content. However, as the package author, no doubt I would find an issue or improvement for the package and publish it. If you were to update the package, it would repeat its initialization process and nuke your customizations. I would prefer not to see you when you’re angry.
  • Not guaranteed. Lastly, you could try and add the NuGet package to only one of your projects, or to a library that isn’t intended as a Xamarin.Forms project.

Above all else, the NuGet documentation clearly states that these files should be treated immutable and not intended to be modified by the consuming project. And since the best place to add the package is immediately after you create the project using a Visual Studio Template, why not just make a Template?

Creating a Multi-Project Template

While Multi-Project Templates have been around for a while, their tooling has improved considerably over the last few releases of Visual Studio. Although there isn’t a feature to export an entire solution as a multi-project, they conceptually work the same way as creating a single project template and then tweaking it slightly.

There are two ways to create a Project Template. The first and easiest is simply to select Project –> Export Template. The wizard that appears will prompt you for a Project and places your template in the My Exported Templates folder.

The second approach requires you to install the Visual Studio SDK, which can be found as an option in the initial installer. When you have the SDK installed, you can create a Project Template as an item in your solution. This project includes the necessary vstemplate files and produces the packaged template every time you build.

image

Effectively, a Project Template is just a zip file with a .vstemplate file in it. A Multi-Project Template has a single .vstemplate that points to templates in subfolders. Here’s how I created mine:

1. Create a Project Template project

Using the Visual Studio SDK, I created a Project Template project to my solution and modified the VSTemplate file with the appropriate details:

<VSTemplate Version="2.0.0" Type="ProjectGroup"
    xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
  <TemplateData>
    <Name>Xamarin.Forms with Caliburn.Micro</Name>
    <Description>Xamarin.Forms project with PCL library.</Description>
    <ProjectType>CSharp</ProjectType>
    <Icon>_icon.ico</Icon>
    <DefaultName>App</DefaultName>
    <ProvideDefaultName>true</ProvideDefaultName>
    <CreateNewFolder>true</CreateNewFolder>
    <RequiredFrameworkVersion>2.0</RequiredFrameworkVersion>
    <SortOrder>1000</SortOrder>
    <TemplateID>Your ID HERE</TemplateID>
  </TemplateData>
  <TemplateContent/>
</VSTemplate>


2. Export Projects and Add to the Project Template project

Next, simply export all the projects in your solution that you want to include in your template. The Project –> Export Template dialog looks like this:

image

Once you’ve exported the projects as templates take each of the zip files and extract them into a subfolder of your Template Project. Then, in Visual Studio, include these extracted subfolders as part of the project. Note that Visual Studio will assign a default Action for each file, so code files will be set to Compile, images will be set as EmbeddedResource, etc. You’ll have to go through each of these files and change the default action to Content, copy if newer. It’s a pain, and I found it easier to unload the project and manually edit the csproj file directly.

3. Configure the Template to include the embedded Projects

Now that we have the embedded projects included in the output, we need to modify the template to point to these embedded templates. Visual Studio has a set of reserved keywords that can be used in the vstemplate and code transforms; $safeprojectname$ is a reserved keyword that represents the name of the current project. My vstemplate names the referenced templates after the name that was provided by the user:

<VSTemplate Version="2.0.0" Type="ProjectGroup"
    xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
  <TemplateData>
    ...
  </TemplateData>
  <TemplateContent>
    <ProjectCollection>
      <ProjectTemplateLink ProjectName="$safeprojectname$" CopyParameters="true">XF\MyTemplate.vstemplate</ProjectTemplateLink>
      <ProjectTemplateLink ProjectName="$safeprojectname$.Android" CopyParameters="true">XF.Android\MyTemplate.vstemplate</ProjectTemplateLink>
      <ProjectTemplateLink ProjectName="$safeprojectname$.UWP" CopyParameters="true">XF.UWP\MyTemplate.vstemplate</ProjectTemplateLink>
      <ProjectTemplateLink ProjectName="$safeprojectname$.iOS" CopyParameters="true">XF.iOS\MyTemplate.vstemplate</ProjectTemplateLink>
    </ProjectCollection>
  </TemplateContent>  
</VSTemplate>

If the ProjectName is omitted, it will use the name within the embedded template.

4. Fix Project References

To ensure the project compiles, we must fix the project references to the PCL library in the iOS, Android and UWP projects. Here we leverage an interesting feature of Multi-Project templates – Visual Studio provides special reserved keywords for accessing properties of the root template project. In this case, we can reference the safeprojectname of the root project using the $ext_safeprojectname$ reserved keyword. And because project references use a GUID to refer to the referenced project, we can provide the PCL project with a GUID that will be known to all the child projects – in this case, we can use $ext_guid1$.

The <ProjectGuid> element in the PCL Project must be configured to use the shared GUID:

<PropertyGroup>
  <MinimumVisualStudioVersion>11.0</MinimumVisualStudioVersion>
  <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
  <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
  <ProjectGuid>{$ext_guid1$}</ProjectGuid>
  <OutputType>Library</OutputType>
  <AppDesignerFolder>Properties</AppDesignerFolder>
  <RootNamespace>$safeprojectname$</RootNamespace>
  <AssemblyName>$safeprojectname$</AssemblyName>
  <FileAlignment>512</FileAlignment>
  <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
  <TargetFrameworkProfile>Profile259</TargetFrameworkProfile>
  <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
  <NuGetPackageImportStamp>
  </NuGetPackageImportStamp>
</PropertyGroup>

In the projects that reference the PCL, the path to the project, project GUID and Name must be also be modified:

<ItemGroup>
  <ProjectReference Include="..\$ext_safeprojectname$\$ext_safeprojectname$.csproj">
    <Project>{$ext_guid1$}</Project>
    <Name>$ext_projectname$</Name>
  </ProjectReference>
</ItemGroup>

5. Fix-ups

Lastly, there will be some other fix-ups you will need to apply. These are things like original project names that appear in manifest files, etc. The templating engine can make changes to any type of file, but you may need to verify that these files have the ReplaceParameters attribute set to True in the .vstemplate file.

Build and Deploy!

With this in place, you can simply compile the Project Template and copy the zip to ProjectTemplates folder. Optionally, you can add a VSIX project to the solution that you can use to bundle our Project Template as an installer that you can distribute to users via the Visual Studio Extensions Gallery.

Happy coding!

submit to reddit

0 comments: