My "Deep Dive Into The Guidance Automation Toolkit" presentation now online!#

Tom's team has been kind enough to put my session of last year's TechDays (then still known as the Developer & IT Pro Days) online on MSDN Chopsticks. You can find my "Deep Dive Into The Guidance Automation Toolkit" presentation at http://www.microsoft.com/belux/msdn/nl/chopsticks/default.aspx?id=10. Everything I said back then is still relevant today, so if you missed it last year you can now catch up for free :-)

And in the light of Software Factory technologies, it also makes a nice preparation for my talk on Domain-Specific Development with Visual Studio DSL Tools next week. My session is scheduled on Thursday March 13 at 10:45. I'm really looking forward to it, and I hope to see you there!

Monday, March 3, 2008 10:39:16 AM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

Interview on Guidance Automation Toolkit#

Right after my session at the Developer & IT Pro days last week, I was interviewed by the Wygwam team - which was pretty cool :-) It also serves as a pretty good 5-minute introduction to Guidance Automation Toolkit, so check it out!


Video: DevITProDays - Jelle

Wednesday, April 4, 2007 12:45:36 PM (Romance Daylight Time, UTC+02:00) #    Comments [3]  | 

 

Developer & IT Pro Days wrap-up#

I had a great time at the Belgian Developer & IT Pro Days in Ghent last week, many thanks to the organizing team (great job Tom, David, Arlindo, Ritchie, Wim and everybody else)!

If you came to my sessions last year, you might remember that I ended a little early (25 minutes!) on my Framework Design talk and a little late (10 minutes!) on the Application Development talk with Steven Wilssens, so I was pretty happy that I finished right on the minute this year :-)

You can download the slides here: A Deep Dive Into The Guidance Automation Toolkit (3 MB). And don't forget about the Guidance Automation Series that goes into much more detail.

Now for a quick shout out to my fellow community members: congratulations Jan on the birth of your first child Fran! From the pictures, it looks like she's already well into the Microsoft spirit :-) And congratulations Bart on joining Microsoft Corp on the WPF team! I'm sure you'll fit right in with the rest of the brainiacs on campus! Say hi to Steven from me :-)

Wednesday, April 4, 2007 12:06:24 PM (Romance Daylight Time, UTC+02:00) #    Comments [0]  | 

 

GAS07: Renaming the solution, signing projects and showing documentation#

[This is episode 7 of the Guidance Automation Series]

It's been a while since the last episode, but I've finally found the time to finish the (long awaited) grand finale of the Guidance Automation Series.

Last time, we looked at class generation using the powerful T4 text templating engine. Armed with all the knowledge up to here, it's already perfectly possible to build an entire guidance package that allows the developer to create Enterprise Library Application Blocks with a minimal amount of effort. As I said in the introduction to the Guidance Automation Series, I'm not going to build that guidance package here. I'll just show you some more tricks you can teach your guidance package to perform... This time, we'll be adding a strong name to our assemblies by signing it with a key and see how we can rename the solution's file name.

Renaming The Solution

We'll start off with the relatively easy task of renaming the solution. Although the developer has already specified a solution name, in a lot of organizations it's required that the solution also adheres to a certain naming convention. And possibly, this depends on some fields that the user can define in the wizard belonging to the Guidance Package, such as a namespace. The only problem is, our Guidance Package doesn't kick in until after the developer has specified a name for the solution file - and now it's obviously too late to rename it. With a little trick, however, it's perfectly possible to change the solution name, e.g. to ApplicationBlockNamespace.ApplicationBlockName.sln in our Guidance Package for Enterprise Library application blocks.

To do this, we will need a custom action class that performs this magic. We can create a custom action by deriving from the Microsoft.Practices.RecipeFramework.Action class. An action can take inputs and create outputs by defining properties that are decorated with the Input or Output attributes:

public class RenameSolutionAction : Action
{
    /// <summary>
    /// The new name of the solution.
    /// </summary>
    private string m_NewSolutionName;

    /// <summary>
    /// Gets or sets the new name of the solution.
    /// </summary>
    [Input(Required = true)]
    public string NewSolutionName
    {
        get
        {
            return m_NewSolutionName;
        }
        set
        {
            m_NewSolutionName = value;
        }
    }

    /// <summary>
    /// Executes the action.
    /// </summary>
    public override void Execute()
    {
    }

    /// <summary>
    /// Performs an undo of the action.
    /// </summary>
    public override void Undo()
    {
    }
}

At this point, our custom action doesn't really do anything but you can see that it overrides the Execute and Undo methods that are called when the action is run and when it needs to be undone.

The actual work of renaming the solution can now be performed in the Execute method by using the EnvDTE objects of the Visual Studio SDK. In our case, we'll perform the rename by simply doing a "Save As" of the solution using the new name, and then deleting the original solution file and its associated suo file:

public override void Execute()
{
    DTE vs = this.GetService<DTE>(true);

    string newFilename = this.NewSolutionName;
    string originalSolutionPath = (string)vs.Solution.Properties.Item("Path").Value;
    string originalSolutionDir = Path.GetDirectoryName(originalSolutionPath);

    // Check if it is an absolute or relative path.
    if (!Path.IsPathRooted(this.NewSolutionName))
    {
        // Relative path from the current solution root.
        newFilename = Path.Combine(originalSolutionDir, this.NewSolutionName);
    }

    // Make sure the destination directory exists.
    Directory.CreateDirectory(Path.GetDirectoryName(newFilename));

    // Save the current solution as the new file name.
    vs.Solution.SaveAs(newFilename);

    // Delete the old solution.
    File.Delete(originalSolutionPath);
    string suoFile = Path.GetFileNameWithoutExtension(originalSolutionPath) + ".suo";
    string suoFilePath = Path.Combine(originalSolutionDir, suoFile);
    File.Delete(suoFilePath);
}

Guidance Automation Tip #3: The vs.Solution.FileName or vs.Solution.FullName properties do not return a value while the solution is being created. You can use "string solutionPath = (string)vs.Solution.Properties.Item("Path").Value;" to get the full path of the solution.

Now that our action is created, we can register it in the Guidance Package's main XML configuration file in the <Actions> section:

<Action Name="RenameSolution" Type="JelleDruyts.EnterpriseLibraryGuidance.Actions.RenameSolutionAction, JelleDruyts.EnterpriseLibraryGuidance">
  <Input Name="NewSolutionName" RecipeArgument="FinalSolutionName" />
</Action>

The action takes an input argument named NewSolutionName (as defined in the RenameSolutionAction class), which is taken from a Recipe Argument named FinalSolutionName. This argument is again a "derived" argument, which is composed of the application block namespace and name through an evaluator:

<Argument Name="FinalSolutionName">
  <ValueProvider Type="Evaluator" Expression="$(ApplicationBlockNamespace).$(ApplicationBlockName).sln">
    <MonitorArgument Name="ApplicationBlockNamespace" />
    <MonitorArgument Name="ApplicationBlockName" />
  </ValueProvider>
</Argument>

Signing The Projects

As a last step, we want to enable the developer to sign the generated projects with a strong-name key. This means we will need to generate a key-file dynamically (by using the sn.exe tool that ships with the .NET SDK), and that we need to configure the generated projects to use the key to sign the assemblies.

In our specific case, signing the Enterprise Library Application Block assemblies has an extra risk: this will only work if Enterprise Library itself is signed because a signed assembly cannot reference an unsigned assembly. Because Enterprise Library is not signed by default, we will need to make this step optional so the developer can choose whether or not to sign the projects. This means we will need an extra argument (that defaults to false) and a field in the wizard:

<Argument Name="EnableStrongNaming" Required="true" Type="System.Boolean">
  <ValueProvider Type="Evaluator" Expression="false" />
</Argument>
...
<Field ValueName="EnableStrongNaming" Label="Enable strong-naming">
  <Tooltip>Only enable strong naming if you have a build of Enterprise Library that is itself strong-named.</Tooltip>
</Field>

Now the trick to signing the projects when they are generated is actually quite simple: there are two settings in the C# project file that are required: SignAssembly must be set to true, and AssemblyOriginatorKeyFile must be set to the path to the key file. We can do this by using recipe arguments in the .csproj template files as such:

<SignAssembly>$EnableStrongNaming$</SignAssembly>
<AssemblyOriginatorKeyFile>$AssemblyOriginatorKeyFile$</AssemblyOriginatorKeyFile>

The developer has already specified the EnableStrongNaming argument through a field in the wizard. Now the AssemblyOriginatorKeyFile needs to point to a valid path to a key file in case strong naming is enabled, or it should be an empty string if strong naming is not enabled. This can be performed by using a custom value provider that checks the EnableStrongNaming argument and determines which string to return: either a string containing the path to the key-file, or an empty string. I won't go into the details here, since Value Providers were already covered in a previous article in this series, but I just want to stress one important point when developing them:

Guidance Automation Tip #4: When writing a Value Provider, returning null means "don't use this value".

This means that if you would return null in the Value Provider that provides a value for the AssemblyOriginatorKeyFile argument and the developer chose not to strong-name the assemblies, the $AssemblyOriginatorKeyFile$ would not get replaced at all in any template! If you want the argument to be replaced with an empty string, be sure to return an empty string from the Value Provider as well:

if (!enabled)
{
    newValue = string.Empty; // Not null!
}
else
{
    newValue = string.Format(@"..\{0}", keyFileName);
}

Now that we have a Value Provider for the path to the key-file that also works when the developer chose not to use strong-naming, we can register the following recipe arguments to define the KeyFileName (again based on the Application Block namespace and name) and the AssemblyOriginatorKeyFile arguments:

<Argument Name="KeyFileName">
  <ValueProvider Type="Evaluator" Expression="$(ApplicationBlockNamespace).$(ApplicationBlockName).snk">
    <MonitorArgument Name="ApplicationBlockNamespace" />
    <MonitorArgument Name="ApplicationBlockName" />
  </ValueProvider>
</Argument>
<Argument Name="AssemblyOriginatorKeyFile">
  <ValueProvider Type="JelleDruyts.EnterpriseLibraryGuidance.ValueProviders.KeyFilePathValueProvider, JelleDruyts.EnterpriseLibraryGuidance"
                 KeyFileNameArgument="KeyFileName" EnableStrongNamingArgument="EnableStrongNaming">
    <MonitorArgument Name="KeyFileName" />
    <MonitorArgument Name="EnableStrongNaming" />
  </ValueProvider>
</Argument>

The only remaining work is to actually generate the key file itself and add it to the Visual Studio solution. This can be implemented as a custom action again, in which we will perform the following work:

public override void Execute()
{
    if (this.EnableStrongNaming)
    {
        EnvDTE.DTE vs = this.GetService<EnvDTE.DTE>(true);
        string solutionPath = (string)vs.Solution.Properties.Item("Path").Value;
        string solutionDir = Path.GetDirectoryName(solutionPath);
        string keyFilePath = Path.Combine(solutionDir, this.KeyFileName);

        // Find the .NET SDK directory to execute the sn.exe command.
        string visualStudioDir = Path.GetDirectoryName(vs.Application.FileName);
        string sdkDir = Path.Combine(visualStudioDir, @"..\..\SDK\v2.0\Bin");
        string snPath = Path.Combine(sdkDir, "sn.exe");
        if (!File.Exists(snPath))
        {
            throw new InvalidOperationException("Could not find the Visual Studio SDK directory to execute the sn.exe command.");
        }

        // Make sure the directory exists.
        Directory.CreateDirectory(Path.GetDirectoryName(keyFilePath));

        // Launch the process and wait until it's done (with a 10 second timeout).
        ProcessStartInfo startInfo = new ProcessStartInfo(snPath, string.Format("-k \"{0}\"", keyFilePath));
        startInfo.CreateNoWindow = true;
        startInfo.UseShellExecute = false;
        Process snProcess = Process.Start(startInfo);
        snProcess.WaitForExit(10000);

        // Add the key file to the Solution Items.
        DteHelper.SelectSolution(vs);
        vs.ItemOperations.AddExistingItem(keyFilePath);

        // The AddExistingItem operation also shows the item in a new window, close that.
        vs.ActiveWindow.Close(EnvDTE.vsSaveChanges.vsSaveChangesNo);
    }
}

This custom action uses an input parameter named KeyFileName which is the recipe argument we defined before. We have also passed in the EnableStrongNaming argument to know if we actually need to generate the key file. We then find the path to the .NET SDK by first looking at the application directory of Visual Studio (vs.Application.FileName) and then using a relative path to the SDK. We check for the presence of the sn.exe application, and start a process (without a window to avoid the DOS box from popping up) to generate the key file. When it's done, we add the key file to the solution. Note the special way of adding a file to the "Solution Items" special folder: we're first selecting the solution in Solution Explorer (through the DteHelper class), and then calling the AddExistingItem method with the path to the item. This seems to be the easiest and most robust way of doing this, since the "Solution Items" folder is handled quite differently and it wouldn't be enough to just create a folder with that name. In the end, we also close the active window, because by default the added item is shown in a new window - which doesn't make a lot of sense for a binary key file.

Now that we have all this infrastructure in place, running the Guidance Package with strong naming enabled results in the following solution, properly renamed, containing the strong name key-file and with both projects enabled for signing:

Showing Documentation

One of the nice added features in the latest version of the Guidance Automation Toolkit is the Guidance Navigator, which provides an overview of the currently loaded Guidance Packages and the recipes they provide. It can also show an HTML page containing more information and links to online resources, to provide continuous guidance to developers using the package.

Showing an HTML page in this Guidance Navigator window is as simple as adding the following XML section to the top of the Guidance Package's main XML file, as the first node inside the main <GuidancePackage> tag:

<GuidancePackage xmlns="http://schemas.microsoft.com/pag/gax-core" ...>
  <Overview Url="Templates\Doc\Overview.html" />

The Url points to the HTML page to be shown, of course, and I've chosen to store this in the Templates\Doc directory. When the solution is created or opened, the Guidance Navigator window will show the page as such:

Where Are We

At this point, we've added some custom actions to rename the solution and we've also given the developer the option to sign the assemblies with a strong name.

I think I've covered quite a lot of scenarios already to end the Guidance Automation Series right here. Any additional tips and tricks I discover will of course be posted here, so keep an eye on my blog if you're interested in the Guidance Automation Toolkit!

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

Monday, September 18, 2006 8:00:28 PM (Romance Daylight Time, UTC+02:00) #    Comments [5]  | 

 

Slides for my Visug session on Guidance Automation Toolkit#

Thanks to everyone that showed up yesterday at the Visug session, where I tried to show you the power of the Guidance Automation Toolkit. It was great to see such an interested crowd, and to get some excellent questions. The meat of the presentation was in the demos, but if you want to take a look at the slides, here you go:

Guidance Automation Toolkit.ppt (1,25 MB)

Thursday, August 10, 2006 9:47:36 AM (Romance Daylight Time, UTC+02:00) #    Comments [1]  | 

 

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.

Monday, July 3, 2006 9:39:13 PM (Romance Daylight Time, UTC+02:00) #    Comments [4]  | 

 

GAS05: Tuning the C# projects#

[This is episode 5 of the Guidance Automation Series]

Last time, we left off with two empty projects that had a project reference between them. If you're still not too thrilled by all that, let's take it one step further: we'll add binary references to Enterprise Library and make sure that the project output is always copied to the Enterprise Library Configuration Tool directory (otherwise, the application block won't show up in the configuration tool). The first one involves knowing where to find those references, the second one involves executing a post-build command that copies the project output to the proper location.

Adding Binary References

Adding a binary reference in itself is pretty straight-forward. The project references are stored in the C# project file (which is an MSBuild file) and can therefore easily be added in the project's template. The only minor difficulty is that we need to provide the hint-path, i.e. the physical location where the assemblies can be found.

In this case, we'll be adding references to Enterprise Library assemblies, so we need to know the directory where Enterprise Library was installed. By default, this is in C:\Program Files\Microsoft Enterprise Library January 2006\bin, but it could have been installed elsewhere so we better verify this with the user. The easiest way, of course, is to make this a recipe argument, just like with the application block's name and namespace:

<Argument Name="EnterpriseLibraryPath" Required="true">
  <ValueProvider Type="Evaluator" Expression="C:\Program Files\Microsoft Enterprise Library January 2006\bin" />
</Argument>

Notice that we're using the ExpressionEvaluatorValueProvider again, but this time without a complex expression. Used in this way, it just sets a default value for the argument that can be modified by the user later on. If we now want to add a binary reference to the Microsoft.Practices.EnterpriseLibrary.Common.dll assembly, for example, we can just add the following lines to the itemgroup containing the references:

<Reference Include="Microsoft.Practices.EnterpriseLibrary.Common, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$EnterpriseLibraryPath$\Microsoft.Practices.EnterpriseLibrary.Common.dll</HintPath>
</Reference>

Notice that the $EnterpriseLibraryPath$ parameter will automatically be replaced by the directory that the user chose in the wizard. So finally, the wizard's page definition will also need to include the new field:

<Field ValueName="EnterpriseLibraryPath" Label="Enterprise Library Path">
  <Editor Type="System.Windows.Forms.Design.FolderNameEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
</Field>

This time, we're also providing an editor that allows the user to select the directory more easily using a folder browser window.

Adding A Post-Build Command

Now that we've referenced Enterprise Library, it's also a good time to make sure that the project output is always copied to the Enterprise Library directory. Otherwise, the application block is quite unuseable. The easiest way to do this is to add a post-build command to both projects that simply copy the project's assemblies to the EnterpriseLibraryPath the user has already chosen. Just like with the binary references, this is just a matter of modifying the MSBuild project file to include a new property group as such:

<PropertyGroup>
  <PostBuildEvent>$PostBuildCommand$</PostBuildEvent>
</PropertyGroup>

Creating the post-build command itself is a little trickier: it depends on the EnterpriseLibraryPath argument, and needs to build a more complex string based on that argument's value. The post-build command itself (as defined in Visual Studio) is simply:

COPY /Y $(TargetPath) "<Enterprise Library Path>"

Where we need to replace <Enterprise Library Path> with the value of the EnterpriseLibraryPath argument. So, how can we provide this value to the $PostBuildCommand$ argument? By building our own Value Provider, of course.

We start off by defining the argument itself:

<Argument Name="PostBuildCommand">
  <ValueProvider Type="JelleDruyts.EnterpriseLibraryGuidance.ValueProviders.PostBuildCommandValueProvider, JelleDruyts.EnterpriseLibraryGuidance"
                 EnterpriseLibraryPathArgument="EnterpriseLibraryPath">
    <MonitorArgument Name="EnterpriseLibraryPath" />
  </ValueProvider>
</Argument>

Notice that the value provider is now of our own type, namely the PostBuildCommandValueProvider we'll be building, and that it monitors the EnterpriseLibraryPath argument (since it depends on it). Furthermore, since the value provider needs access to the value of the EnterpriseLibraryPath argument, we also need to specify that argument's name, which is done through a custom EnterpriseLibraryPathArgument attribute on the ValueProvider node.

The code for the Value Provider isn't all that difficult, either. It's a class that inherits from the ValueProvider base class and that also implements the IAttributesConfigurable interface to support that custom EnterpriseLibraryPathArgument attribute. It can then override the OnArgumentChanged method that gets called when a monitored argument has changed, and re-generate the post-build command as such:

public override bool OnArgumentChanged(string changedArgumentName, object changedArgumentValue, object currentValue, out object newValue)
{
    // Get the name of the argument that defines the EnterpriseLibraryPath.
    string enterpriseLibraryPathArgumentName = m_Attributes[EnterpriseLibraryPathArgumentAttribute];
    
    // Get the dictionary that contains the recipe arguments.
    IDictionaryService dictionary = this.GetService<IDictionaryService>(true);

    // Get the current value of the EnterpriseLibraryPath argument.
    string enterpriseLibraryPath = (string)dictionary.GetValue(enterpriseLibraryPathArgumentName);

    // Create the post-build command.
    newValue = string.Format("COPY /Y \"$(TargetPath)\" \"{0}\"", enterpriseLibraryPath);
    return true;
}

Now we have an argument containing the post-build command that automatically contains the proper directory based on the Enterprise Library directory the user has chosen in the wizard dialog:

Where Are We

We've seen how to add binary references to the C# projects by modifying the MSBuild files and gathering user input through the wizard. We've also built a custom Value Provider that depends on the value of another argument, and used that as a post-build command. Next time, we'll see how we can generate some classes so that the application block can already be added to the Enterprise Library Configuration Tool.

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

Thursday, June 29, 2006 9:19:25 PM (Romance Daylight Time, UTC+02:00) #    Comments [1]  | 

 

GAS04: Adding Project References#

[This is episode 4 of the Guidance Automation Series]

Last time, we did a whole lot of work just to generate one very empty C# project. Just think of that as the healthy dish before the dessert: it's probably necessary but not really what you've been waiting for. This time, we'll really start using the power of Guidance Automation to generate a second project and add a project reference to it.

Adding Another Project

Now that we have seen how to generate the runtime project for the Enterprise Library Application Block, we'll just do the same thing to generate the design-time project. By convention, this has the same name as the runtime project, with ".Configuration.Design" added at the end. This is easily done in the solution's vstemplate file by adding a second project to the ProjectCollection node as such:

<ProjectTemplateLink ProjectName="$ApplicationBlockNamespace$.$ApplicationBlockName$.Configuration.Design">Projects\DesignTime\DesignTime.vstemplate</ProjectTemplateLink>

For this to work, we also need the DesignTime.vstemplate file in the proper location, but that's pretty easy to do: we can just copy the Runtime.vstemplate file and associated project files and make a few basic modifications to change the name. So with very little additional work, we have now generated two empty projects already.

Now of course, this design-time project will need information from the runtime project (e.g. the configuration types of the application block that are used by the runtime but need to be configured in the design-time project), so we need to add a project reference to it. Project references are actually stored in the referencing project's project file, but this isn't something we can generate out-of-the-box like before because it uses the GUID from the referenced project. This GUID is generated by Visual Studio at the time the solution is unfolded (remember the $guid1$ parameter) and we don't have access to that GUID outside the scope of that file. But Visual Studio knows how to add Project References, so we actually want to have the environment perform some action for us.

Actions

Actions are actually a big part of Guidance Automation, and they represent pieces of code that can run at the end of a recipe (and in other places too, but let's stick to the current problem). There are a lot of predefined actions in the Guidance Automation framework, but you can also define your own. To use an action, it can be registered in the Actions section in the recipe xml file, so that each one will be run in sequence at the end of the recipe. Actions have input and output arguments that can be configured, and it is important to note that these are strongly typed, which means that you're really passing objects from and actions and not just strings. The input arguments can either come from the recipe (e.g. a value the user has entered in the wizard), or they can be the output of another action.

Adding The Project Reference

To add a Project Reference, the predefined AddProjectReferenceAction seems to be just what we need here. It takes two input arguments: the referring project and the referenced project. Both are of the type EnvDTE.Project, meaning that they represent the actual project automation objects in Visual Studio. So we need a way to retrieve these projects, and that's where another action, aptly named GetProjectAction comes into play. This searches for a project based on its name and puts the project in an output argument so we can chain the two together as follows:

<Actions>
  <Action Name="GetRuntimeProject" Type="Microsoft.Practices.RecipeFramework.Library.Actions.GetProjectAction, Microsoft.Practices.RecipeFramework.Library">
    <Input Name="ProjectName" RecipeArgument="RuntimeProjectName" />
    <Output Name="Project" />
  </Action>
  <Action Name="GetDesignTimeProject" Type="Microsoft.Practices.RecipeFramework.Library.Actions.GetProjectAction, Microsoft.Practices.RecipeFramework.Library">
    <Input Name="ProjectName" RecipeArgument="DesignTimeProjectName" />
    <Output Name="Project" />
  </Action>
  <Action Name="AddProjectReference" Type="Microsoft.Practices.RecipeFramework.Library.Solution.Actions.AddProjectReferenceAction, Microsoft.Practices.RecipeFramework.Library">
    <Input Name="ReferringProject" ActionOutput="GetDesignTimeProject.Project" />
    <Input Name="ReferencedProject" ActionOutput="GetRuntimeProject.Project" />
  </Action>
</Actions>

Notice that the AddProjectReference action has inputs that come from the other two actions, and these in turn have inputs that come from the recipe. Now if you remember, the user didn't specifically provide the name of the projects to be generated, only the namespace and the name of the application block. We can use these arguments to derive the actual project names using the built-in ExpressionEvaluatorValueProvider.

Value Providers

A value provider is a class that provides values to arguments. Just like a Converter can convert and validate arguments, value providers can put values into arguments. Some of the built-in value providers can return the currently selected class in Visual Studio or a certain project by its name, but there's also a pretty powerful ExpressionEvaluatorValueProvider that can return values based on other arguments through expressions.

In this case, we simply want to build up the name of the runtime project by appending the two fields the user has entered:

<Argument Name="RuntimeProjectName">
  <ValueProvider Type="Evaluator" Expression="$(ApplicationBlockNamespace).$(ApplicationBlockName)">
    <MonitorArgument Name="ApplicationBlockNamespace" />
    <MonitorArgument Name="ApplicationBlockName" />
  </ValueProvider>
</Argument>

By also supplying the MonitorArgument values, the expression will automatically be re-evaluated if one of the monitored arguments has changed. We can now base the design-time project's name on this argument by appending a suffix to the project name as follows:

<Argument Name="DesignTimeProjectSuffix">
  <ValueProvider Type="Evaluator" Expression="Configuration.Design" />
</Argument>
<Argument Name="DesignTimeProjectName">
  <ValueProvider Type="Evaluator" Expression="$(RuntimeProjectName).$(DesignTimeProjectSuffix)">
    <MonitorArgument Name="RuntimeProjectName" />
    <MonitorArgument Name="DesignTimeProjectSuffix" />
  </ValueProvider>
</Argument>

Side note: it would also have been possible to use the expression "$(RuntimeProjectName).$(DesignTimeProjectSuffix).Configuration.Design" directly (without creating the DesignTimeProjectSuffix argument) but unfortunately there's a problem with the expression parser that makes the wizard very slow in this case.

Now that we have the names for both projects stored in arguments, we can also replace the project names in the vstemplate files that were still made up of the application block's name and namespace individually. E.g., we can replace ProjectName="$ApplicationBlockNamespace$.$ApplicationBlockName$" by ProjectName="$RuntimeProjectName$".

Where Are We

At this point, we've seen how to generate a second project with a project reference by using actions that are chained together. We've also seen how to use value providers that put values into arguments and how to use these values in actions. Next time, we'll see how to build our own value providers and have them interact with other arguments. Furthermore, we'll add binary references to some of the Enterprise Library assemblies and add post-build events to the projects. Stay tuned!

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

Wednesday, June 28, 2006 9:06:58 PM (Romance Daylight Time, UTC+02:00) #    Comments [3]  | 

 

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.

Tuesday, June 27, 2006 10:35:19 PM (Romance Daylight Time, UTC+02:00) #    Comments [8]  | 

 

GAS02: Creating A Guidance Package#

[This is episode 2 of the Guidance Automation Series]

In this first real part of the Guidance Automation Series, we'll start off slowly and easily... If you want to follow along, be sure to install both the Guidance Automation Extensions (GAX) and the Guidance Automation Toolkit (GAT).

Once you've installed GAT, you'll notice a new project type in Visual Studio called "Guidance Packages". This will contain all the packages that were installed on this machine. At this time, a first package is even already registered: the "Guidance Package" package.

See, creating a guidance package from scratch yourself would be quite a hassle, so this über-package will generate a sample package for you that you can use to immediately start building your own. People that can handle more recursive thinking than me often call this a Software Factory Factory, because it creates something that will in turn create something else.

Anyway, if you create a new project using this "Guidance Package" package, you'll get quite a lot of code that shows off a big number of the available features, as well as two additional projects that together form the setup project for your package. You can now build this entire solution, right-click the guidance package project and select "Register Guidance Package" from the context menu.

When the registration process has finished, you're ready: your first guidance package has been built and deployed on your local machine. You can test it by opening another instance of Visual Studio, where you should now notice your own package in the "Guidance Packages" project type.

Using the MSI that was built by the setup project, you can now easily deploy this Guidance Package to any developer pc that has the Guidance Automation Extensions installed.

Guidance Automation Tip #1: Always keep your guidance package open in one dedicated instance of Visual Studio and test the package in new instances every time.

Notice that registering the package took quite some time, and unfortunately this is life in the Guidance Automation world: everytime you want to verify or test your package, you'll have to register it and start another Visual Studio to test it in. So my development cycle typically looks as follows: make modifications to the package, build the package, register the package, open another Visual Studio, test the package, make modifications to the package, ... In other words: think twice before you think your modification is ok :-)

The good news is that with this new release, a new "Quick Register" option has been added to the context menu, as you can see in the screenshot above. You can use this to drastically speed up the development process if you don't have any new recipes or templates that need to be registered or if you didn't change any HostData sections that define the integration with Visual Studio. If you did any of these things, you will need to re-register the entire package, though.

Now that we have the basics covered, let's remove all the pre-generated parts from the package that aren't needed so that we're left with a clean and empty Guidance Package. This basically means deleting all the files in the package except the two xml files in the project root: these form the package definition and are used by GAX to register it. This empty solution will be the basis on which we'll build the Enterprise Library Guidance Package in the next installment of this series.

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

Monday, June 26, 2006 10:09:55 PM (Romance Daylight Time, UTC+02:00) #    Comments [2]  | 

 

GAS01: Introduction To The Guidance Automation June 2006 CTP#

[This is episode 1 of the Guidance Automation Series]

Great news: the June 2006 CTP of Guidance Automation (GAX and GAT) has just been released! Although it's still labeled a CTP (because it's unsupported), I personally think of this as the 1.1 release of a great tool I've been using extensively the last couple of weeks.

If you've never heard of the Guidance Automation Extensions (GAX) and the Guidance Automation Toolkit (GAT), this is the elevator pitch straight from the Guidance Automation community site:

With the Guidance Automation Toolkit, you can make reusable code and pattern assets directly available in Visual Studio 2005. It is designed to simplify integrating reusable code into applications. This lets architects automate processes, and guide their developers through complex procedures that would otherwise need to be thoroughly documented.

So in other words: the Guidance Automation Toolkit provides a means for a solution architect or lead developer to deploy consumable guidance to developers. If you've ever published a 40-page document full of guidelines, do's and don'ts on how to structure solutions and projects in your company and what the namespaces and class names should be, if the developers need to be able to start new projects quickly, if you have a need for code generation, or all of the above - this toolkit is for you. Microsoft is itself using this technology to publish their Software Factories, like the Web Service Software Factory, so that says something about the quality and future of this fine piece of software.

Just to get some basic vocabulary right: the Guidance Automation Extensions (GAX) is the runtime part that needs to be installed on the machines of the developers that want to use the Guidance Packages. The Guidance Automation Toolkit (GAT) is the authoring part that helps you create the Guidance Packages and the setup projects to install them.

New in this release of GAX are the Guidance Navigator window, which provides a separate window listing all the recipes in the currently loaded guidance package (thereby greatly enhancing their discoverability) and the fact that the state of the guidance package (which actions can be performed where in the solution, for example) isn't persisted in the per-developer .suo file anymore but in a separate .gpstate file so team development becomes possible. A very handy new feature of GAT is the Quick Register action that can drastically speed up the development process.

In a series of posts I'll commonly call the Guidance Automation Series (or GAS for short, if you will), I'll be showing you how to build a Guidance Package with this new release for a sample scenario that's related to that other big project from the Microsoft patterns & practices group: Enterprise Library 2.0. I'll be implementing a Guidance Package that allows you to quickly start building your own application blocks that plug in nicely with the rest of Enterprise Library. The point is not to provide a full-blown Guidance Package with all the bells and whistles required to start writing these application blocks, the idea is to show you how to create the Guidance Package - so you can go the extra mile of adding all the bells and whistles yourself :-) If you need more details on how to build the application blocks themselves, I can highly recommend the "Speed Development With Custom Application Blocks For Enterprise Library" article by Mark Seemann that was just published on MSDN.

So, if you're into this kind of stuff, keep reading this blog :-)

Monday, June 26, 2006 9:50:14 PM (Romance Daylight Time, UTC+02:00) #    Comments [1]  | 

 

All content © 2014, Jelle Druyts
On this page
Top Picks
Statistics
Total Posts: 351
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.3.12105.0

Sign In