GAS06: Generating Classes#

[This is episode 6 of the Guidance Automation Series]

Last time, we added references to Enterprise Library and made sure the build output was copied to the configuration tool directory. Now to make the application block show up in this configuration tool, it needs to register some classes and commands so that the tool knows how to display it. Without implementing the full works here, let's see how we can already generate some classes that will serve as an excellent starting point to implement the rest of the application block.

Adding Default Classes

Actually, we've already seen how to add classes in the first episode of this series: the AssemblyInfo.cs file was added to the project by including it in a ProjectItem inside the project's vstemplate file and by including it in the C# project file (so that MSBuild will build it). The only extra feature we'll be using here is to change the target file name of each file to include the name of the application block.

To make it possible to add the application block in the configuration tool, we have to define a ConfigurationDesignManager class and register it in the AssemblyInfo.cs file. So we need to update that file and add a bunch more, including a CommandRegistrar, a NodeMapRegistrar and a root settings node. For the purpose of all these classes and how they should be implemented, I'll gladly refer to Mark Seemann's excellent article on "Speed Development With Custom Application Blocks For Enterprise Library" again. All we need to do here is provide the minimum amount of information to get it registered with the configuration tool and leave the rest up to the actual developer of the application block.

So we add all the necessary files to the project template, include them in the C# project file and register them in the vstemplate file as such:

<ProjectItem ReplaceParameters="true" TargetFileName="$ApplicationBlockName$CommandRegistrar.cs">CommandRegistrar.cs</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="$ApplicationBlockName$ConfigurationDesignManager.cs">ConfigurationDesignManager.cs</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="$ApplicationBlockName$NodeMapRegistrar.cs">NodeMapRegistrar.cs</ProjectItem>
<ProjectItem ReplaceParameters="true" TargetFileName="$ApplicationBlockName$SettingsNode.cs">SettingsNode.cs</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="$ApplicationBlockName$SettingsNode.bmp">SettingsNode.bmp</ProjectItem>

Notice the TargetFileName attribute here, to specify a new name for the file so that it includes the name of the application block as it was entered by the user. Also note that we're adding a bitmap file here (without replacing any parameters, to make sure we're not accidentally messing with the binary data), which is also added in the C# project file but then as an embedded resource:

<ItemGroup>
  <EmbeddedResource Include="$ApplicationBlockName$SettingsNode.bmp" />
</ItemGroup>

That's all there is to it. If we now register and run the Guidance Package, we'll get a project that includes all of these files. If we build it (without touching anything), it will automatically get copied into the configuration tool's directory (because of the post-build event), and we can already add the application block to an application directly!

Generating Classes

Now that we already have a root node in the project, suppose we want to add more nodes for the configuration tool. The number of nodes in the application block depends on what the developer has in mind and could even be unknown when the application block is being created. So in stead of adding more classes at the time the solution is unfolded, let's add a command to Visual Studio that allows us to generate a class on-demand. This can be done by adding a RecipeReference to a Visual Studio Template or by adding a separate template for the class in the Templates\Items directory.

  • With a RecipeReference, you have full control over the process: when the recipe is executed, you can choose to show a dialog box prompting for more information or you can go directly to work by executing actions. A drawback is that you also need to include the actual actions to generate the class content and add it to the project, although this isn't all too hard.
  • With a Template Reference, you will always get the "Add new item" dialog box that allows the developer to choose the new class' filename. This can be a drawback if you need to control the filename as well, but this mechanism does make it easier to generate the class and add it to the project.

Since adding the class as a Template Reference is very similar to adding a solution as a Template Reference (like we've already been doing all along), let's implement the new nodes as RecipeReferences. To allow the developer to add dynamically created items to a project, we need to attach a reference to a recipe in that project's vstemplate file. In this case, we'll add the following information to the vstemplate file of the DesignTime project:

<WizardData>
  <Template xmlns="http://schemas.microsoft.com/pag/gax-template" SchemaVersion="1.0">
    <References>
      <RecipeReference Name="AddConfigurationNode" Target="/" />
    </References>
  </Template>
</WizardData>

The References node has been added to register the AddConfigurationNode recipe to the root of the project (specified by the target). If there would be subdirectories, you could scope the target down by setting Target="/Nodes", for example.

The AddConfigurationNode recipe needs to be defined in the package's main xml file as such:

<Recipe Name="AddConfigurationNode">
  <Caption>Configuration Node...</Caption>
  <Description>Adds a Configuration Node class.</Description>
  <HostData>
    <Icon Guid="FAE04EC1-301F-11d3-BF4B-00C04F79EFBC" ID="4542" />
    <CommandBar Name="Project Add" />
  </HostData>
  <Arguments>
    <!-- User Input -->
    <Argument Name="NodeName" Required="true">
      <Converter Type="Microsoft.Practices.RecipeFramework.Library.Converters.CodeIdentifierStringConverter, Microsoft.Practices.RecipeFramework.Library"/>
    </Argument>
    <!-- Derived Arguments -->
    <Argument Name="CurrentProject" Type="EnvDTE.Project, EnvDTE, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
      <ValueProvider Type="Microsoft.Practices.RecipeFramework.Library.ValueProviders.FirstSelectedProject, Microsoft.Practices.RecipeFramework.Library" />
    </Argument>
    <Argument Name="TargetFile">
      <ValueProvider Type="Evaluator" Expression="$(NodeName).cs">
        <MonitorArgument Name="NodeName" />
      </ValueProvider>
    </Argument>
    <Argument Name="TargetNamespace">
      <Converter Type="Microsoft.Practices.RecipeFramework.Library.Converters.NamespaceStringConverter, Microsoft.Practices.RecipeFramework.Library"/>
      <ValueProvider Type="Evaluator" Expression="$(CurrentProject.Properties.Item('DefaultNamespace').Value)" />
    </Argument>
  </Arguments>
  <GatheringServiceData>
    <Wizard xmlns="http://schemas.microsoft.com/pag/gax-wizards" SchemaVersion="1.0">
      <Pages>
        <Page>
          <Title>Configuration Node</Title>
          <Fields>
            <Field ValueName="NodeName" Label="Node Name" InvalidValueMessage="Must be a valid .NET identifier (e.g. it shouldn't contain spaces or special characters)." />
          </Fields>
        </Page>
      </Pages>
    </Wizard>
  </GatheringServiceData>
  <Actions>
    <Action Name="GenerateClass" Type="Microsoft.Practices.RecipeFramework.VisualStudio.Library.Templates.TextTemplateAction, Microsoft.Practices.RecipeFramework.VisualStudio.Library"
				Template="Text\ConfigurationNode.cs.t4" >
      <Input Name="TargetNamespace" RecipeArgument="TargetNamespace" />
      <Input Name="NodeName" RecipeArgument="NodeName" />
      <Output Name="Content" />
    </Action>
    <Action Name="AddClass" Type="Microsoft.Practices.RecipeFramework.Library.Actions.AddItemFromStringAction, Microsoft.Practices.RecipeFramework.Library"
				Open="true">
      <Input Name="Content" ActionOutput="GenerateClass.Content" />
      <Input Name="TargetFileName" RecipeArgument="TargetFile" />
      <Input Name="Project" RecipeArgument="CurrentProject" />
    </Action>
  </Actions>
</Recipe>

The recipe definition starts off with the regular caption and description elements. The HostData section defines where the recipe should be shown in Visual Studio; in this case we add it only to the project's "Add" context menu (where the "Add new item" and "Add existing item" menus also live) and supply it with an appropriate icon from the C# Project dll again. Then, there's a regular argument that the developer can fill in through the wizard, but the more interesting part is the definition of the other arguments: we're using a new type of value provider that returns the currently selected project in Visual Studio (so we can add the new class to it). There are also some more uses for the ExpressionEvaluatorValueProvider: a simple one to define the filename based on the class name, and a pretty complex one that finds the default namespace from the current project by drilling down into its properties (the nested expression is evaluated at runtime and executed through reflection). Finally, the actions section defines two distinct stages: a first one to generate the class content by executing the TextTemplateAction, which transforms a T4 text template using a quite advanced text processing engine. Afterwards, the generated class content is added to the currently selected project by using the AddItemFromStringAction.

Finally, the template itself looks like this:

<#@ template language="C#" #>
<#@ assembly name="System.dll" #>
<#@ property processor="PropertyProcessor" name="TargetNamespace" #>
<#@ property processor="PropertyProcessor" name="NodeName" #>
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text;

using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Configuration.Design;

namespace <#= this.TargetNamespace #>
{
    internal sealed class <#= this.NodeName #> : ConfigurationNode
    {
    }
}

This templating syntax looks quite a lot like ASP.NET, so creating and understanding these templates should be relatively straight-forward. We first declare some properties that are inputs from the recipe declaration above. These will actually become strongly-typed properties on the page template object, so we can refer to them later on by using this.NodeName, for example. At this point, the generated class is extremely simple, but you can actually perform very powerful code generation with this T4 templating engine by inlining arbitrary C# or VB.NET code within these ASP.NET-like tags that will drive the code generation process.

Notice that you also need to register the assemblies you require for the code generation process. In this case, we just register the System.dll assembly. If, for example, we needed to use the Visual Studio automation dll (EnvDTE.dll) in the template, we would need to add the following declarations:

<#@ assembly name="C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\PublicAssemblies\EnvDTE.dll" #>
<#@ import namespace="EnvDTE" #>

Failing to do so will give you the following nasty error: "CS0246: Compiling transformation: The type or namespace name 'EnvDTE' could not be found (are you missing a using directive or an assembly reference?)"

With this in place, it's now possible to add a new configuration node through the project's context menu:

Where Are We

At this point, we know how to add default classes to a generated project that have a customized name. Furthermore, we've touched the surface on the very powerful T4 text templating engine that's part of the Guidance Automation framework. Next time, we'll define a custom action that generates a strong name key and configure the projects to automatically be signed with it. We'll also automatically change the solution's file name to something more meaningful.

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

Tuesday, July 04, 2006 10:59:02 AM (Romance Standard Time, UTC+01:00)
I really love the Guidance Automation Series, it’s the LOST of architecture. You rock dude!
Tuesday, July 04, 2006 4:23:10 PM (Romance Standard Time, UTC+01:00)
Hi,
I Found Absolutely FREE PlayBoy & Penthouse:
http://www.playmates-girls.com
http://www.oxpe.net
If I find something else I'll inform you.
Best Regards, Yuriy
Wednesday, September 06, 2006 10:41:50 PM (Romance Standard Time, UTC+01:00)
Hi Jelle, great step by step guide, and very well commented !
Thx for sharing !
Stephan Depoorter
Monday, January 29, 2007 11:13:02 AM (Romance Standard Time, UTC+01:00)
奥斯丁
Comments are closed.
All content © 2010, 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: 346
This Year: 0
This Month: 0
This Week: 0
Comments: 525
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