How to Create a Visual Studio 2012 Project Template - Part 2: Automatic Export

by Larry Spencer Tuesday, December 4, 2012 6:33 AM

In the last post, we saw how to create a basic Project Template in Visual Studio 2012. One aspect of the process that we'd like to improve is the export and packaging of the Template.

Visual Studio does offer a C# Project Template project type for this purpose.

Exporting with Visual Studio's C# Project Template

 

What you get with this project type is a project-within-a-project.

 

 

The inner .csproj will form the basis for projects instantiated from the Template. Unfortunately, that inner project is not itself compilable because it is filled with the template-substitution parameters we'll learn about in a later post. (As you might guess, the parameters take on useful values when the template is used to create an actual project.) For example, here is the Class1.cs file. Look at all the red squigglies!

 

 

Even the .csproj file itself has substitution variables. As a result, you can't even do Project -> Properties on it to choose the settings that you want as the basis for your Project Template. You must tinker with the raw .csproj file.

Let's invent something better.

Exporting with a Custom Exporter

Recall from the last post that exporting a Project Template (File -> Export Template) consists of creating a ZIP file which has the project's source, an icon, and a .vstemplate file that serves as the manifest. In addition, you'll probably want to introduce template-substitution parameters like the ones we just saw, but File -> Export Template does not do that for you. You must modify the contents of the ZIP by hand. Ugly!

In the downloadable solution (ProjectTemplateTutorial.zip (271.98 kb) that illustrates this series, you'll find a project called Fws.VS2012.TemplateExporter. It's a simple console application that adds those parameters to the source and creates the ZIP file. There's no need to go through TemplateExporter's source line-by-line here. I just want to highlight two aspects of the process.

Adding Template Parameters

First, let's see how we modify the project file. This illustrates the process of adding template parameters.

 

static internal string[] CreateModifiedProject(string[] projectLines)
{
    var searchAndReplace = new Tuple<Regex, string>[]
    {
        Tuple.Create(new Regex(@"(^ *<ProjectGuid>\{)(.+)(\}</ProjectGuid> *$)"), "$1$$guid1$$$3"),
        Tuple.Create(new Regex(@"(^ *<RootNamespace>)(.+)(</RootNamespace> *$)"), "$1$$namespace$$$3"),
        Tuple.Create(new Regex(@"(^ *<AssemblyName>)(.+)(</AssemblyName> *$)"), "$1$$namespace$$$3"),
        Tuple.Create(new Regex(@"(^ *<.*>)(\.\.\\\.\.\\\.\.\\)(.+</.*> *$)"), "$1$$fwsdir$$$3"),
        Tuple.Create(new Regex(@"(^ *<[^ ]+ [^ ]+="")(\.\.\\\.\.\\\.\.\\)(.*$)"), "$1$$fwsdir$$$3"),
    };

    for (int ix = 0; ix < projectLines.Length; ++ix)
    {
        foreach (var sr in searchAndReplace)
        {
            projectLines[ix] = sr.Item1.Replace(projectLines[ix], sr.Item2);
        }
    }

    return projectLines;
}

 

As you can see, we're just processing the project file as text (too lazy to do the whole XML thing). We loop through all the lines and use regular expressions to make a few changes.

Each project should have a unique GUID, and the Visual Studio templating system gives you a few GUIDs to work with. The parameters $guid1$ through $guid10$ will come to you with unique values each time. We're using the first one as our ProjectGuid.

Recall that one of our objectives was to make the generated assembly's default namespace and assembly name match. We therefore put the $namespace$ parameter in the appropriate places.

So far, we have used built-in parameters. The final two Tuples are used to change certain things to use a custom parameter, $fwsdir$. In the next post, we'll see how to create it. In the meantime, I'll just say that it will be a relative path to the root of our source tree, C:\Software\Fws. We saw in the last post that when Visual Studio generates projects from templates it tends to mess up relative references to linked file such as our signing key. In our development environment, all linked references happen to be to files under C:\Software\Fws. However, we don't want to hard-code that path so we make relative paths back to the Fws directory and go from there. For example, the Fws.VS2012.UnitTestProject.Template project in the sample is signed with C:\Software\Fws\Signing\Fws.snk. That translates to ..\..\..\Singing\Fws.snk. The search-and-replace above plugs $fwsdir$ in place of those relative references.

These references crop up in project files in two ways that I have encountered.

Sometimes they will be the InnerText of an XML element such as <HintPath>

<Reference Include="Moq">
  <HintPath>..\..\..\Moq\Moq.dll</HintPath>
</Reference>

That's what the second-to-last Tuple takes care of.

And sometimes they are in an attribute.

<ItemGroup>
  <None Include="..\..\..\Signing\Fws.snk">
    <Link>Fws.snk</Link>
 </None>
</ItemGroup>

The final Tuple accounts for that case. 

In addition to modifying the .csproj file, our custom exporter modifies the AssemblyInfo.cs. If you write a custom exporter, you might want to do even more.

Creating the ZIP File

Once the source files have been modified in memory, we write them to the ZIP along with the icon and .vstemplate file.

 

void CreateZip(string[] modifiedProject,string[] modifiedAssemblyInfo)
{
    var zipFullyPathedFileName = Path.Combine(_targetDir, Properties.Resources.OutputZipName);
    using (Package zip = Package.Open(zipFullyPathedFileName, FileMode.Create))
    {
        AddToZip(zip, "/" + _sourceProjectFileName, modifiedProject, MediaTypeNames.Text.Xml);
        AddToZip(zip, "/Properties/AssemblyInfo.cs", modifiedAssemblyInfo, MediaTypeNames.Text.Plain);
        AddToZip(zip, "/", Path.Combine(_exporterSourceDir, "Fws.ico"), "image/ico");
        AddToZip(zip, "/", Path.Combine(_exporterSourceDir, "UnitTestProject.vstemplate"), MediaTypeNames.Text.Xml);
    }
    Console.WriteLine("Created {0}.", zipFullyPathedFileName);
}

 

Conclusion

We now have a ZIP file that looks just like it would if we had used File -> Export Template and then added template parameters to the result.

In the next post, we'll work more magic with custom code. We'll then have all the pieces we need to create the project that packages the template for deployment.

Tags: ,

All | Talks

Comments (2) -

6/17/2013 1:41:51 AM #

Hello, Thank you for your posts here. i  have to create a vsix project template for a multi-project application that we are developing at work. We've recently moved to vs 2012 at work. i have vs 2012 premium edition installed, but can't find any of the extensibity templates either in the Add New Project dialog or the Tools-> Extensions And Updates dialog...what am i missing? Thanks.

Didi Masiala Canada

6/17/2013 2:14:16 AM #

Hi.. Never mind.. I figured it out.. i need to install the VS SDK.

Didi Masiala Canada

Pingbacks and trackbacks (1)+

Add comment

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.