Jelle Druyts .NET Consultant
Just another ignorant weirdo from Antwerp, Belgium trying to make sense out of it all
[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.
ProjectItem
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.
ConfigurationDesignManager
CommandRegistrar
NodeMapRegistrar
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.
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.
Target="/Nodes"
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.
HostData
ExpressionEvaluatorValueProvider
actions
TextTemplateAction
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.
this.NodeName
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.