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, January 29, 2007 11:03:17 AM (Romance Standard Time, UTC+01:00)
<a href="http://gjefoomq.com">sudmkxtp</a> qkljhizq http://twfuusym.com slkpmvtu fbfjonzf [URL=http://bllleqdd.com]aezyqmvg[/URL]
Monday, April 23, 2007 7:37:21 PM (Romance Standard Time, UTC+01:00)
Hi Jelle,

Just like to say thanks for your series. It has helped me a lot to get going on Guidance and Automation.

Martin
Tuesday, July 03, 2007 11:31:41 AM (Romance Standard Time, UTC+01:00)
How can I change the name of some projects present under one solution.
Wednesday, July 11, 2007 9:54:49 AM (Romance Standard Time, UTC+01:00)
Hi Jelle,

We have created a Recipe that creates our own custom project. I am able to run this Recipe from Guidance package manager. but i am unable to add bind it to the solution. We want this Recipe available on the solution. I have already set the HostData.CommandBar attribute to Solution in my Recipe. But i m clueless why my Recipe do not show up when i right click on my solution. Can you help me to solve this issue

Your series was very helpful in our work. It gave us a comprehensive understanding of GAT/GAX.

regards
Dhawal
Sunday, July 15, 2007 5:37:29 PM (Romance Standard Time, UTC+01:00)
Aka-

I'm not sure how to change the name of a project after it has been created but it might be possible through the project's properties collection or even as a real property or method on the project class. Both would require you to interop with EnvDTE though, but it might be possible.

Dhawal-

Have you tried adding a RecipeReference to the WizardData/Template/References node in the solution's vstemplate file? In the samples I built, this reference is added to the Design Time project (not the solution) but it should work equally well if you add it to the solution's vstemplate file I guess...

Good luck,

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

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

Powered by:
newtelligence dasBlog 2.0.7226.0

Sign In