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]  | 

 

IIS virtual directory names and mapped file extensions#

Just a quick note to mention a little quirk in Internet Information Services (IIS) I discovered recently: when you have a virtual directory name that ends in a registered file extension, you'll get an HTTP 404 error when browsing to that directory.

We were using http://localhost/MyCompany.MyProject.Svc/MyService.asmx as our web service endpoint, but that kept giving us HTTP 404's. At first, we thought it had to do with the dots in the name (causing us to change our guidelines and drop part of our naming consistency) but I later found out it has to do with the .svc file extension being mapped to the ASP.NET isapi dll. So even though it's clearly a directory name and not a file, we still got the 404.

So we changed our guidelines (once more) to MyCompany.MyProject.Svc.WebServices and all was fine again...

Blog | Programming | .NET | ASP.NET | Quirks
Sunday, June 25, 2006 11:45:29 AM (Romance Daylight Time, UTC+02:00) #    Comments [0]  | 

 

Unable to load oramts.dll? Try the release version#

We were quite amazed recently when we discovered the root cause of the following error message:

System.DllNotFoundException: Unable to load DLL 'oramts.dll': The specified module could not be found. (Exception from HRESULT: 0x8007007E).

We were getting this error on our application servers (not on our developer workstations) when using distributed transactions from .NET 2.0 with Oracle 9i (coordinated by MSDTC).

It turns out that the version of oramts.dll (which provides DTC transaction support for Oracle) as installed by the Oracle 9i Release 2 CD is a debug build, rather than a release build. (Let that sink in for a moment: Oracle is shipping debug builds of their software...). As you can see in the following screenshot of the dll in DependencyWalker, this debug build has a dependency on MSVCRTD.DLL, which is a debug build of the Microsoft C Runtime Library:

This debug version is installed on our workstations (because we have Visual Studio installed), but of course not on our application servers, causing the loader exception shown above. Copying the MSVCRTD.DLL file to the System32 directory solved the problem - temporarily of course (you don't want to be installing debug builds all over the application servers because of an Oracle dll).

Anyway, if you suffer from this issue as well, you can get a patch from Oracle containing the proper dll. You can easily tell the difference by looking at the file size: 137 KB for the release build versus 192 KB for the debug build.

Sunday, June 25, 2006 11:21:07 AM (Romance Daylight Time, UTC+02:00) #    Comments [1]  | 

 

The Visual Studio 2005 Toolbox, UserControls and Solution Folders#

There's a pretty inconvenient bug in the Windows Forms designer in Visual Studio 2005, which apparently is a known issue but not scheduled to be fixed anytime soon. Here's the scoop and a (slightly sucky) workaround...

If you have a User Control in your project, it will automatically get added to the Toolbox window so you can drag it onto your designer surface. However, it doesn't show up in the Toolbox if the project is inside a Solution Folder (which is a virtual folder in your solution). It would be really nice if it were also possible to drag the user control from the Solution Explorer onto the designer surface, but unfortunately that's not supported.

So a possible workaround is to take the simplest type of control available (I tend to go for a plain old Panel) and add that to the form in stead of the user control you really want to add. Then switch to the designer-generated code (e.g. MyForm.Designer.cs) and change both the declaration of the panel (e.g. "private System.Windows.Forms.Panel panel1;") and the instantiation (e.g. "this.panel1 = new System.Windows.Forms.Panel();" inside the "Windows Form Designer generated code" region) to your user control type (e.g. MyNamespace.MyUserControl).

It sucks, but it works.

Blog | Programming | .NET | Quirks | VS.NET | WinForms
Friday, June 23, 2006 6:20:59 PM (Romance Daylight Time, UTC+02:00) #    Comments [5]  | 

 

A fix for "Could not access network location" when installing a Visual Studio Addin#

Just a quick hint: if you're installing addins for Visual Studio and the installer quits with a "Could not access network location "\\SomePath\Visual Studio\Addins", this probably means your "My Documents" folder is stored on a network drive, causing the installer to fail (for some reason I haven't yet discovered).

I found a pretty quick workaround, which is temporarily changing the location of the "My Documents" folder to a path on a local drive. You can do this by changing the registry key (all usual disclaimers apply, I didn't touch your computer so I didn't break it) "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurentVersion\Explorer\User Shell Folders\Personal". Note that the installer won't actually use this path to store anything, so it's safe to use any path you deem fit. If the addin installed properly after this hack, don't forget to change the registry key back to its original value and you should be fine.

I haven't tried it for other similar issues, but it might even work with any other type of software that breaks with the same error message.

Blog | Programming | .NET | VS.NET
Wednesday, June 21, 2006 6:59:39 PM (Romance Daylight Time, UTC+02:00) #    Comments [0]  | 

 

Creating a setup package with multiple optional custom actions#

I've recently needed to create an installer with custom actions that only need to be run when the user selects certain options in the installation wizard. It's pretty easy to do once you know the pitfalls, but it took me some time to figure out so I figured I might as well post it for future reference...

One option is to create a separate assembly for each custom action that can be selected, and then setting its Condition property so that it is only executed if the user chose the corresponding option.

However, this could result in quite a lot of extra assemblies (currently three in my case, but it might become more), which I try to avoid to make it easier to deploy and use these custom actions. So I wanted one assembly containing all the installer classes that could need to be run, and then decide inside that assembly which ones to execute based on the user's choice. This is what worked for me:

  • Pass in the user's choices (e.g. the property values of the checkboxes) through the custom action's CustomActionData property, e.g. "/OPTION1=[OPTION1] /OPTION2=[OPTION2]" in case there are two checkboxes with OPTION1 and OPTION2 properties. Also pass in all other data that needs to be available for the individual installers (note that these may conflict, in which case you'll have to create different assemblies anyway).
  • Mark each individual installer with the [RunInstaller(false)] attribute. Otherwise, the installer will be executed no matter what. Side note: my first attempt at solving this problem was to check inside each installer's Install method if it needed to be executed based on the context parameters, but this caused all sorts of problems with the savedState dictionary.
  • Create one main installer class with [RunInstaller(true)]. This is the one we will add the decision-making logic in, and which will keep the master savedState dictionary.
  • In this main installer, override the Install method and, before calling base.Install, add the individual installers to its Installers property depending on the parameters that were passed in. Also, remember which installers were added (which options were selected) by putting them in the stateSaver object. Example:
    if (this.Context.Parameters["OPTION1"] == "1")
    {
      this.Installers.Add(new Option1Installer());
      stateSaver["OPTION1"] = "1";
    }
    It's important to only call base.Install after this, because the base implementation will then install all the child installers that were just added.
  • Override the Commit and Rollback methods, and before calling the base class implementation, add the installers again depending on the values you put in the savedState object. This is necessary because these methods will be called on new instances of your installer class, so you need to re-add them to the Installers property. Example:
    if (savedState["OPTION1"] == "1")
    {
      this.Installers.Add(new Option1Installer());
    }
  • Do the same thing for the Uninstall method, this way you are sure that you're only uninstalling the parts that were actually installed. This is important, because when uninstalling, the parameters that are passed in do not remember the user's initial choices (i.e. OPTION1 might be set to "1" when uninstalling, even if the user didn't select that choice at installation time).

This is what worked for me, if there are simpler ways of achieving the same thing, don't hesitate to make suggestions :-)

Blog | Programming | .NET | VS.NET
Wednesday, June 21, 2006 6:55:35 PM (Romance Daylight Time, UTC+02:00) #    Comments [0]  | 

 

Feline Beta#

I think I've been spending a wee bit too much time with beta software lately...

Last night, I woke up from a dream that involved my... [dramatic pause] cat. So I was getting a new beta of my cat for some reason, and I noticed that all of a sudden she had a white piece of fur (normally, she's all black (black!)). And all I could think about whas the fact that my cat seemed to have a regression bug.

Go figure.

I think I'll have to go blow the dust off of my VB6 box just to get my life back on track. See you after my rehab :-)

(Now that I think about it, I never suspected I'd ever write a post about my cat. Oh no, I'm becoming a Dear Diary blogger!)

Thursday, June 15, 2006 7:45:34 AM (Romance Daylight Time, UTC+02:00) #    Comments [2]  | 

 

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