GAS03: Generating A C# Project#

[This is episode 3 of the Guidance Automation Series]

Last time, we left off with an empty guidance package that we will now be using to build the Enterprise Library guidance package. To scope it down a bit, we'll only be supporting C# in this example - but of course the same principles apply to other languages. As a first step, we'll set up the package so that it will generate two projects: the runtime project that contains the logic for the application block, and the design-time project that contains the user interface logic for the configuration tool.

Templates And Recipes

Before diving in, you need to know that a guidance package is basically a combination of Visual Studio Templates and Guidance Automation Recipes.

  • Visual Studio Templates are the standard way to define types of solutions, projects and classes that the developer can create in Visual Studio (e.g. they show up in the Create New Project/Item dialog boxes). Visual Studio Templates are defined as .vstemplate files in the Templates directory of the project, in a subdirectory that defines the type of template (e.g. Solution or Projects).
  • Recipes are automated activities that follow a series of instructions to execute some action that a developer would otherwise need to do manually (e.g. creating a number of projects in Visual Studio and adding references to them). Recipes are defined in the main xml file (i.e. not the TypeAlias.xml file but the xml file named after your guidance package), which means configuring this part will require some xml tinkering - although fortunately there is pretty good IntelliSense support in Visual Studio.

The two are linked by adding a recipe reference to the vstemplate file that will make sure the recipe is executed at the time the solution is "unfolded" (which is just a fancy term to say "created" in Visual Studio).

The Solution Template

Armed with this knowledge, we can now create a basic vstemplate file in the Templates\Solutions directory that contains the template for the Application Block solution:

<VSTemplate Version="2.0" Type="ProjectGroup" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
  <TemplateData>
    <Name>Application Block</Name>
    <Description>Guidance Package that creates a new Enterprise Library Application Block.</Description>
    <ProjectType>CSharp</ProjectType>
    <SortOrder>91</SortOrder>
    <Icon>ApplicationBlock.ico</Icon>
    <CreateNewFolder>false</CreateNewFolder>
    <DefaultName>ApplicationBlock</DefaultName>
    <ProvideDefaultName>true</ProvideDefaultName>
  </TemplateData>
  <TemplateContent>
    <ProjectCollection>
      <ProjectTemplateLink ProjectName="$ApplicationBlockNamespace$.$ApplicationBlockName$">Projects\Runtime\Runtime.vstemplate</ProjectTemplateLink>
    </ProjectCollection>
  </TemplateContent>
  <WizardExtension>
    <Assembly>Microsoft.Practices.RecipeFramework.VisualStudio, Version=1.0.51206.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</Assembly>
    <FullClassName>Microsoft.Practices.RecipeFramework.VisualStudio.Templates.UnfoldTemplate</FullClassName>
  </WizardExtension>
  <WizardData>
    <Template xmlns="http://schemas.microsoft.com/pag/gax-template" SchemaVersion="1.0" Recipe="CreateApplicationBlock">
    </Template>
  </WizardData>
</VSTemplate>

There are a few interesting things to note here:

  • The TemplateData section covers the metadata for the solution: its icon, name and description as they appear in the New Project dialog box, the project type, and some other properties.
  • The TemplateContent section defines the structure of the solution; in this case we define a project collection (we'll be adding another project later on) that contains one project template for the runtime part of the application block. When the solution is created, the template defined in the Runtime.vstemplate file will also be "unfolded".
  • The project's name is defined as "$ApplicationBlockNamespace$.$ApplicationBlockName$"; this is the first appearance of parameters (enclosed in $-signs). These values will be replaced at runtime by arguments from the Guidance Recipe, which we will define later on, to form the project name in Visual Studio.
  • The Template node in the WizardData section establishes the link between the vstemplate and the corresponding "CreateApplicationBlock" recipe that needs to be defined in the main xml file.

Guidance Automation Tip #2: All Visual Studio template files (vstemplates and their contents such as project files, code files and images) must have their "Build Action" set to "Content" and "Copy to Output Directory" to "Copy Always". The Guidance Automation Toolkit adds an item in the Templates directory's context menu that will do this for you.

The Project Template

At this point, we have a template for the solution that contains a link to a project template and to a recipe. The Runtime.vstemplate project template looks like this:

<VSTemplate Version="2.0.0" Type="Project" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
  <TemplateData>
    <Name>Application Block Runtime Project</Name>
    <Description>A project template for the runtime part of an Application Block.</Description>
    <Icon Package="{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}" ID="4547" />
    <ProjectType>CSharp</ProjectType>
    <SortOrder>20</SortOrder>
    <CreateNewFolder>true</CreateNewFolder>
    <DefaultName>Runtime</DefaultName>
    <ProvideDefaultName>true</ProvideDefaultName>
  </TemplateData>
  <TemplateContent>
    <Project File="Runtime.csproj" TargetFileName="$ApplicationBlockNamespace$.$ApplicationBlockName$.csproj" ReplaceParameters="true">
      <ProjectItem ReplaceParameters="true">Properties\AssemblyInfo.cs</ProjectItem>
    </Project>
  </TemplateContent>
  <WizardExtension>
    <Assembly>Microsoft.Practices.RecipeFramework.VisualStudio, Version=1.0.51206.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</Assembly>
    <FullClassName>Microsoft.Practices.RecipeFramework.VisualStudio.Templates.UnfoldTemplate</FullClassName>
  </WizardExtension>
  <WizardData>
    <Template xmlns="http://schemas.microsoft.com/pag/gax-template" SchemaVersion="1.0">
    </Template>
  </WizardData>
</VSTemplate>

Some highlights from this file:

  • The icon from the package {FAE04EC1-301F-11d3-BF4B-00C04F79EFBC} is the ID of an icon in the C# Project dll, typically located at C:\Program Files\Microsoft Visual Studio 8\VC#\VCSPackages\csproj.dll. This dll can be opened in Visual Studio so you can pick another icon from this dll if you want, but you can also use a custom icon like we did in the solution's template above.
  • The TemplateContent section now defines a project that is created from the Runtime.csproj file. This is a regular C# project file that can again contain some parameters that will be replaced at runtime (if the ReplaceParameters attribute is set to true, anyway).
  • Within the project, we also include a Project Item (typically a file within the project), which will in this case be a boilerplate AssemblyInfo.cs file that the project will already contain.
  • There is no link to a recipe in the Template node here, since this project will only be unfolded by the solution template defined above - which already contains a reference to a recipe.

The Project File

This is the content of the Runtime.csproj file:

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>8.0.50727</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>$guid1$</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>$safeprojectname$</RootNamespace>
    <AssemblyName>$safeprojectname$</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <DocumentationFile>bin\Debug\$safeprojectname$.xml</DocumentationFile>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <DocumentationFile>bin\Release\$safeprojectname$.xml</DocumentationFile>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSHARP.Targets" />
</Project>

Again, some interesting points to look at here:

  • There is no magic here, this is just an MSBuild project file for C# that you can tweak as much as you want.
  • Not only can we use the $ApplicationBlockName$ and other parameters we defined in the recipe, but we also have some standard parameters that can be used (see Template Parameters on MSDN for more information). The $guid1$ parameter will be replaced by a unique GUID (you can define up to 10 different GUIDs like this), the $safeprojectname$ will be replaced by the name provided by the user in the New Project dialog box, with all unsafe characters and spaces removed.
  • Both for the Debug and Release configurations, I've set TreatWarningsAsErrors to true (to enforce the best practice that warnings should be taken seriously) and I've enabled the output of an XML documentation file. The combination of these two settings also enforces that all public members must have XML comments, another best practice I always try to enforce.
  • The ItemGroup section defines that the AssemblyInfo.cs file we included in the project template is part of the project and needs to be compiled.

The Recipe

Now that we have all the templates in place, we still need to define the recipe that will enable the user to automate the creation of the solution and its projects. In its simplest form, you can think of a recipe as a wizard that collects user data - but as we will see later on in this series, it can do a lot more. We've currently defined two custom parameters in the template files: $ApplicationBlockName$ and $ApplicationBlockNamespace$. This means we need to collect this information from the user when the solution is being created. The following recipe definition does just that:

<?xml version="1.0" encoding="utf-8" ?>
<GuidancePackage xmlns="http://schemas.microsoft.com/pag/gax-core"
    Name="JelleDruyts.EnterpriseLibraryGuidance" 
    Caption="Enterprise Library Guidance"
    Description="Provides guidance around the creation of Enterprise Library Application Blocks"
    Guid="2cac5b9c-a04f-4a49-8a56-3ee5d63bd83f" 
    SchemaVersion="1.0">
  <Recipes>
    <Recipe Name="CreateApplicationBlock">
      <Caption>Create a new Enterprise Library Application Block</Caption>
      <Arguments>
        <Argument Name="ApplicationBlockName" Required="true">
          <Converter Type="Microsoft.Practices.RecipeFramework.Library.Converters.CodeIdentifierStringConverter, Microsoft.Practices.RecipeFramework.Library" />
        </Argument>
        <Argument Name="ApplicationBlockNamespace" Required="true">
          <Converter Type="Microsoft.Practices.RecipeFramework.Library.Converters.NamespaceStringConverter, Microsoft.Practices.RecipeFramework.Library" />
        </Argument>
      </Arguments>
      <GatheringServiceData>
        <Wizard xmlns="http://schemas.microsoft.com/pag/gax-wizards" SchemaVersion="1.0">
          <Pages>
            <Page>
              <Title>Application Block Information</Title>
              <LinkTitle>Application Block</LinkTitle>
              <Help>
                Enter the Application Block name and namespace.
              </Help>
              <Fields>
                <Field ValueName="ApplicationBlockName" Label="Application Block Name" InvalidValueMessage="Must be a valid .NET identifier (e.g. it shouldn't contain spaces or special characters)." />
                <Field ValueName="ApplicationBlockNamespace" Label="Application Block Namespace" InvalidValueMessage="Must be a valid .NET namespace identifier (e.g. it shouldn't contain spaces or special characters)." />
              </Fields>
            </Page>
          </Pages>
        </Wizard>
      </GatheringServiceData>
    </Recipe>
  </Recipes>
</GuidancePackage>

Some remarks on this file:

  • Recall that the "CreateApplicationBlock" recipe that is defined here is referenced by the solution's template, so this is the recipe that will be used to gather the data from the user.
  • The Arguments section defines all the recipe arguments that are defined, and can be replaced in the template using parameters. In this case, we have two arguments, ApplicationBlockName and ApplicationBlockNamespace, which are both required. They also have converters associated with them that can validate the arguments (e.g. to make sure they are valid .NET identifiers or namespaces). If the argument is invalid, it will immediately become visible to the user. Likewise, a required argument will have a light yellow background in the wizard, so it's immediately clear which arguments need to be filled in and which can be skipped.
  • The GatheringServiceData section contains a wizard with one page and two fields, one for each argument we want to collect.

Running The Guidance Package

Now that we have all the parts covered, we can finally register and test our package. It will show up as the "Application Block" project type in Visual Studio (with the proper icon and description):

When we create the solution, a wizard will pop up that allows us to enter the information required to generate the projects. Notice that the fields have a light yellow background and that we get an error icon and a tooltip if some argument is invalid:

When we correct the information and press Finish, our first skeleton project for the Enterprise Library Application Block has been created:

Note that the project was correctly named using the combination of the namespace (JelleDruyts.EnterpriseLibrary) and name (ServiceAgents) that was entered in the wizard.

Where Are We

At this point, we've seen how to define Visual Studio Templates that accept parameters and create solution files, project files and project items. We can already impose some guidance and best practices by tweaking the projects (e.g. to treat warnings as errors). We've also seen how to define a recipe that gathers information from the user to pass along to the templates.

You might say, "well, that's an awful lot of work to create a blank project", and you would be absolutely right. But of course, this is just the beginning. And now that we have the basics covered, the fun can really start! Stay tuned for the next episode, in which we'll take this guidance package a step further.

Download the source code for the current state of the Guidance Package.

Wednesday, June 28, 2006 12:14:19 PM (Romance Standard Time, UTC+01:00)
Jelle, your images in your posts don't show up in an rss aggregator. I mailed you about it yesterday...
Wednesday, June 28, 2006 8:13:17 PM (Romance Standard Time, UTC+01:00)
Nico,

Thanks for letting me know. My aggregator (RSS Bandit) does show the images, I guess it adds the base url itself based on the site url. As a temporary workaround, I've made the url's absolute so it should work out for now. I'll look into a better solution later. Does the problem also occur in the Atom feed published at this blog (http://jelle.druyts.net/SyndicationService.asmx/GetAtom)?

Thanks,

Jelle
Thursday, June 29, 2006 9:51:49 AM (Romance Standard Time, UTC+01:00)
Yes I checked both of your feeds. Atom does support xml:base though so you could use that, I don't know of any solution for dealing with rss though. I'd just stick to absolute paths, cause I think that a feed is or should be an independent entity on the net (aggregrators should be able to process it entirely independently of the entity that created the feed, in this case your blog), but hey, that's just my opinion :)
Wednesday, July 19, 2006 3:59:06 PM (Romance Standard Time, UTC+01:00)
I wasn't able to follow along from the write up. It wasn't clear to me what I needed to do in this example.

It looks like I just need to create three xml files. Is that correct?

The location of the files isn't clear to me. Also I am curious your method of creating these files.

Thanks
Tom
Friday, October 20, 2006 8:42:10 AM (Romance Standard Time, UTC+01:00)
Hello Jelle

Have you tried providing the default name for solution based on an argument in the recipe?

For example you have the following in the solution's template:

<DefaultName>ApplicationBlock</DefaultName>

Assume that you have an argument in the recipe named $DefaultSolutionName$ and try to change the solutions template to:

<DefaultName>$DefaultSolutionName$</DefaultName>

This however does not seem to work because in the dialog I will see the string "$DefaultSolutionName$" as the name of the solution.
Friday, October 20, 2006 8:44:00 AM (Romance Standard Time, UTC+01:00)
Ignore my previous post, stupid me. This will off course not work because you need to create a solution first before the recipes execute.
Friday, October 20, 2006 11:07:35 AM (Romance Standard Time, UTC+01:00)
Gabriel,

That's right, the recipe executes only after the solution name is chosen. However, if you look at my final episode in the series, I do show a way to change the solution name after it has been created - which could solve your problem.
Monday, October 23, 2006 4:14:45 PM (Romance Standard Time, UTC+01:00)
Jelle

I have also tried to work with zero impact projects but the GAX doesn't seem to like that.

Gabriel
Comments are closed.
All content © 2012, Jelle Druyts
On this page

Recent Photos
www.flickr.com
This is a Flickr badge showing public photos from Jelle Druyts. Make your own badge here.
Advertising
Top Picks
Statistics
Total Posts: 350
This Year: 0
This Month: 0
This Week: 0
Comments: 530
Archives
Sitemap
Disclaimer
This is my personal website, not my boss', not my mother's, and certainly not the pope's. My personal opinions may be irrelevant, inaccurate, boring or even plain wrong, I'm sorry if that makes you feel uncomfortable. But then again, you don't have to read them, I just hope you'll find something interesting here now and then. I'll certainly do my best. But if you don't like it, go read the pope's blog. I'm sure it's fascinating.

Powered by:
newtelligence dasBlog 2.0.7226.0

Sign In