Team LiB   Previous Section   Next Section

9.2 The VS.NET Wizard Engine

All of the wizards installed with VS.NET are templates that are executed by the wizard engine, which is a COM class whose ProgID is VsWizard.VsWizardEngine.7.1. The wizard engine's job is to display a UI (if required), collect the input from that UI, execute a script, and (potentially) copy template files. The script's job is to take whatever data was entered into the UI and use this to modify the template files if necessary.

VS.NET knows to use the wizard engine because the wizard's .vsz file specifies that the VsWizard.VsWizardEngine.7.1 class should be used. Example 9-4 shows an example of this. The standard parameters you can place in a .vsz file that the wizard engine understands are listed in Table 9-2. Two of these—WIZARD_NAME and WIZARD_UI—are mandatory. WIZARD_NAME tells the wizard engine which wizard to run—it will look for a directory of the specified name in the language's wizard files directory, as specified in Table 9-2. The WIZARD_UI flag indicates whether the wizard will present a UI or just add the specified item straightaway.

Table 9-2. Standard wizard engine parameters for .vsz files

Parameter

Description

ABSOLUTE_PATH

An optional absolute path to the wizard. Not usually required, as VS.NET will look for the wizard in one of the directories listed in Table 9-3 , based on the WIZARD_NAME.

HTML_FILTER

File extensions that the wizard uses for HTML. Required only if something other than .htm is in use.

HTML_PATH

The path to the HTML files for this wizard. By default, VS.NET will look for an HTML folder under the main wizard folder.

IMAGES_PATH

The path of images to be used by the HTML. By default, VS.NET will look for an Images folder under the main wizard folder.

MISC_FILTER

Files that get copied into the Misc folder in the Solution Explorer. Specified as a semicolon-delimited list.

PRODUCT_INSTALLATION_DIR

The installation directory of the language for which the wizard is being executed. By default, this will be either the VC#, VJ#, VB7, or VC7 subdirectory of the VS.NET installation directory.

PROJECT_TEMPLATE_NAME

Name of the template file used to create a project. This defaults to template.inf.

PROJECT_TEMPLATE_PATH

Path to the wizard files. This defaults to one of the folders listed in Table 9-3.

PROJECT_TYPE

The type of project. The default will be appropriate to the template's language (e.g., CSPROJ or VBPROJ).

RELATIVE_PATH

If no ABSOLUTE_PATH is specified, the RELATIVE_PATH can be used to specify from the location of the wizard files relative to PRODUCT_INSTALLATION_DIR. The WIZARD_NAME will be appended to this to form the actual directory. Not typically used, as wizards are normally directly beneath the PRODUCT_INSTALLATION_DIR.

SCRIPT_COMMON_PATH

The directory containing the common script files, relative to PRODUCT_INSTALLATION_DIR. You would not normally change this—you will usually want to have access to the standard common scripts.

SCRIPT_FILTER

File extension filter for files to be placed in the Scripts folder of the project (e.g., js and vbs.

SCRIPT_PATH

The path to the script file for this wizard. The default is START_PATH\Scripts.

START_PATH

Never set in the .vsz—the wizard engine automatically sets this to the path it has determined as the location of the wizard.

TEMPLATE_FILTER

Any file extensions to be placed into the Templates directory.

TEMPLATES_PATH

Path to the wizard's template files. Usually START_PATH\Templates.

WIZARD_NAME

Name of the wizard. This must be the name of the directory that contains the wizard.

WIZARD_UI

A Boolean that indicates whether or not this wizard shows a UI or not. TRUE means it shows a UI, FALSE means that it does not.

The script and HTML files that make up each wizard are placed underneath the directories listed in Table 9-3. For example, the built-in C# Class Library project template is in a folder named VC#\VC#Wizards\CSharpDLLWiz.

Table 9-3. Wizard file locations

Language

Wizard file location

C#

VC#\VC#Wizards

VB

VB7\VBWizards

C++

VC7\VCWizards

J#

VJ#\vjsharpwizards

9.2.1 Wizard Execution

All wizards that use the wizard engine follow the same execution sequence. First, the wizard's UI is shown if it has one. Then the wizard's script is executed.

Not all wizards need to present a UI, so the initial UI step is optional. When a UI is present, it is made up of any number of HTML files. The purpose of the UI is to collect input from the user and make it available to the script. The HTML files therefore contain special tags that indicate which fields contain values that represent user settings. Once the UI stage is complete, the wizard engine loads and executes the wizard's script. The script must be written in JScript in a file called default.js. The following sections describe how to write the UI and script files.

9.2.1.1 The UI

Wizards that use the wizard engine have HTML-based user interfaces. When the wizard runs, the wizard engine displays the first HTML page in a dialog. If the UI has multiple pages in its UI, the left side of the dialog will present a series of links allowing each individual page to be accessed. The main page is a file called default.htm. This must be in a subdirectory called HTML\<Locale ID>. If you are writing for the U.S. locale the ID is 1033.

The MSDN Library provides a complete list of locale IDs on the "Locale ID (LCID) Chart" page in the Microsoft Scripting Technologies documentation. This can be found by typing locale ID into the VS.NET Help Index, which can be opened with Help Index... (Ctrl-Alt-F2).

With multipage user interfaces, the user will not be forced to view every page—unlike some wizard UIs, VS.NET wizards are not sequential. (They behave more like an HTML frameset.) Since you cannot be sure that the user will even look at any page other than the first one, your wizard should supply reasonable defaults for all values.

When the user clicks the Finish button, the wizard engine will execute the script contained in the default.js file, where you will, of course, need to access the values that the user typed in. Fortunately, the wizard engine reads these for you and makes them available to your script and templates.

The VS.NET wizard engine provides a mechanism that deals with both setting default values in a wizard UI and retrieving user input. All such values are passed between the UI and the wizard engine using the <SYMBOL> tag. The <SYMBOL> tag is used to declare variables that represent input fields in the wizard. Example 9-5 is an extract from one of the ATL wizards, showing how this tag is used.

Example 9-5. SYMBOL tags from the ATL Wizard HTML
<SYMBOL NAME="SAFE_PROJECT_NAME" TYPE=text></SYMBOL>
<SYMBOL NAME="UPPER_CASE_PROJECT_NAME" TYPE=text></SYMBOL>
<SYMBOL NAME="LIB_NAME" TYPE=text></SYMBOL>
   
<SYMBOL NAME="DLL_APP" TYPE=checkbox VALUE=true></SYMBOL>
<SYMBOL NAME="EXE_APP" TYPE=checkbox VALUE=false></SYMBOL>
<SYMBOL NAME="SERVICE_APP" TYPE=checkbox VALUE=false></SYMBOL>
   
<SYMBOL NAME="MERGE_PROXY_STUB" TYPE=checkbox VALUE=false></SYMBOL>
<SYMBOL NAME="SUPPORT_MFC" TYPE=checkbox VALUE=false></SYMBOL>
<SYMBOL NAME="SUPPORT_COMPLUS" TYPE=checkbox VALUE=false></SYMBOL>
<SYMBOL NAME="SUPPORT_COMPONENT_REGISTRAR" TYPE=checkbox
        VALUE=false></SYMBOL>
   
<SYMBOL NAME="COMPREG_REGISTRY_FORMAT" TYPE=text></SYMBOL>
<SYMBOL NAME="LIBID_REGISTRY_FORMAT" TYPE=text></SYMBOL>
<SYMBOL NAME="APPID_REGISTRY_FORMAT" TYPE=text></SYMBOL>
   
<SYMBOL NAME="SOURCE_FILTER" TYPE=text></SYMBOL>
<SYMBOL NAME="INCLUDE_FILTER" TYPE=text></SYMBOL>
<SYMBOL NAME="RESOURCE_FILTER" TYPE=text></SYMBOL>
   
<SYMBOL NAME="CODE_PAGE" TYPE=text></SYMBOL>
<SYMBOL NAME="YEAR" TYPE=text></SYMBOL>
   
<SYMBOL NAME="ATTRIBUTED" TYPE=checkbox VALUE=true></SYMBOL>

Any symbol defined in this way can be used in any of the HTML files that make up the user interface. The usual way of doing this is to associate a control with the symbol. For example, the ATTRIBUTED symbol, which selects whether the generated ATL project will use attributes, has a corresponding checkbox in the UI. Such controls are associated with their symbols through the ID attribute. Example 9-6 shows an excerpt of the checkbox tag associated with the ATTRIBUTE symbol.

Example 9-6. Checkbox associated with a symbol
<INPUT CLASS="CheckBox" TYPE="checkbox" ID="ATTRIBUTED" ACCESSKEY="A">

Many of the SYMBOL tags in Example 9-5 have a VALUE attribute. This is used to specify default settings, although default values will not be populated automatically—a little manual intervention is required.

The wizard engine exposes symbols to the scripts on the UI pages. It does this through a series of objects called the wizard engine helper object model, which is accessible through the window.external script object. When an HTML UI is first loaded, the script in the HTML file typically uses the wizard engine object to load default values from the SYMBOL tags into the corresponding controls. This is done by calling the SetDefaults method, as shown in Example 9-7. (The <BODY> tag shown at the top of the example illustrates the usual way of ensuring that the InitDocument function is called when the page is first displayed.)

Example 9-7. Initializing field default values
<BODY ONLOAD="InitDocument(document);">
    . . .
   
<SCRIPT LANGUAGE="JSCRIPT">
   
function InitDocument(document)
{
   setDirection(  );
   
   if (window.external.FindSymbol("DOCUMENT_FIRST_LOAD"))
   {
      var L_WizardDialogTitle_Text = "My Wizard";
      window.external.AddSymbol("WIZARD_DIALOG_TITLE",
                 L_WizardDialogTitle_Text);
      window.external.SetDefaults(document);
   }
   window.external.Load(document);
   InitControls(  );
}
   
</SCRIPT>

The initial call to setDirection sets text direction as left to right or right to left according to the locale. This function is supplied by the shared script files, Script.js and Common.js. To use these from script in your UI, you will need to include them explicitly, as shown in Example 9-8.

Example 9-8. Making standard script files accessible in HTML
<SCRIPT>
    var strPath = "../../../";
    strPath += window.external.GetHostLocale(  );
    var strScriptPath = strPath + "/Script.js";
    var strCommonPath = strPath + "/Common.js";
    document.scripts("INCLUDE_SCRIPT").src = strScriptPath;
    document.scripts("INCLUDE_COMMON").src = strCommonPath;
</SCRIPT>

The call to AddSymbol in Example 9-7 illustrates that script code can set symbol values at runtime without needing to declare them in the list of SYMBOL tags. This particular symbol, WIZARD_DIALOG_TITLE, has the side effect of setting the window title.

This code also calls both SetDefaults and Load. SetDefaults parses the document looking for SYMBOL tags and initializes the wizard engine's internal symbol tables with the specified default values. Load scans the HTML document looking for HTML controls with IDs that match the name of the SYMBOL tags and sets their values. (In other words, this is where the fields get set to their default locations.)

9.2.1.2 The script and the templates

Every wizard must have a script file called default.js. It contains the code that will be run once the UI stage of the wizard is complete. (For wizards that don't have a UI, this script will be run as soon as the wizard is launched.) This file must be in the Scripts\<Locale ID> subdirectory of the wizard's installation directory. (The Locale ID will be 1033 if you are using U.S. English.) Every wizard must also have a templates.inf file, located in the Templates\<Locale ID> subdirectory. This file contains a list of files that the wizard engine should copy to the project directory. The files that are to be copied must live in the same directory as the templates.inf file.

The wizard engine makes a common script file, common.js, available to all default.js files. Each of the languages supported by VS.NET (i.e., C#, J#, C++, and VB.NET) provides its own common.js file in its wizard folder's script directory. The wizard engine will expect your default.js script to contain an OnFinish function, which will be called when the user clicks the Finish button on the UI. (If your wizard has no UI, this function will be called as soon as the user decides to create a new project or item of your wizard's type.) The OnFinish function is responsible for instructing the wizard engine to munge and copy the appropriate template files into the project directory.

Your template.inf file must contain a list of template files to be copied into the project. Each filename appears on its own line. The listed files are text files that will be used as the basis for new files that are added to the project. However, files are not quite copied across verbatim—the wizard gets the opportunity to make modifications. These modifications are made using template directives. Template directives are markers in text files that indicate replaceable or optional sections. They can be applied to any of the files that are in the templates directory, including the templates.inf file itself. Example 9-9 shows a typical example.

Example 9-9. Template file with directives
using System;
   
namespace [!output SAFE_NAMESPACE_NAME]
{
    /// <summary>
    /// Summary description for [!output SAFE_CLASS_NAME].
    /// </summary>
    public class [!output SAFE_CLASS_NAME]
    {
        public [!output SAFE_CLASS_NAME](  )
        {
            //
            // TODO: Add constructor logic here
            //
        }
    }
}

This is a fairly simple template that generates a C# source file containing a class definition. The template directives are the blocks enclosed with square brackets. In Example 9-9, all of the directives are of the form [! output SYMBOL]. When the wizard engine copies a template, it will replace all output directives with the value of the named symbol.

The SAFE_NAMESPACE_NAME and SAFE_CLASS_NAME symbols are generated automatically by the wizard engine. They will typically be the project's default namespace and a default class name such as Class1.

So when the wizard that contains this template is run, the resulting file will look something like this:

using System;
   
namespace ClassLibrary1
{
    /// <summary>
    /// Summary description for Class1.
    /// </summary>
    public class Class1
    {
        public Class1(  )
        {
            //
            // TODO: Add constructor logic here
            //
        }
    }
}

Let's follow the execution of the default.js and the templates.inf files. The C# Class Library Wizard's templates.inf file has two files in it:

File1.cs
assemblyinfo.cs

When the user clicks OK in the New Project dialog or Open in the Add New Item dialog, the IDE passes control to the wizard engine. When the user clicks Finish on the UI, the wizard engine loads default.js and calls the OnFinish function. (With wizards that have no UI, this method will be run immediately.) Example 9-10 shows a typical wizard's OnFinish function.

Example 9-10. Example default.js OnFinish method
function OnFinish(selProj, selObj)
{
    try
    {
        var strProjectPath = wizard.FindSymbol("PROJECT_PATH");
        var strProjectName = wizard.FindSymbol("PROJECT_NAME");
        var strSafeProjectName = CreateSafeName(strProjectName);
        wizard.AddSymbol("SAFE_PROJECT_NAME", strSafeProjectName);
   
   
        var proj = CreateCSharpProject(strProjectName, strProjectPath,
                       "defaultemplate directivel.csproj");
   
        var InfFile = CreateInfFile(  );
        AddReferencesForClass(proj);
        AddFilesToCSharpProject(proj, strProjectName,
                 strProjectPath, InfFile, false);
        proj.Save(  );
    }
    catch(e)
    {
        if( e.description.length > 0 )
            SetErrorInfo(e);
        return e.number;
    }
    finally
    {
        if( InfFile )
            InfFile.Delete(  );
    }
}

There are two parameters to the OnFinish function. These parameters change depending on the type of wizard, but generally the first parameter will be an object reference to the current project. The second parameter can be a reference to an object being added (e.g., a file object when the wizard being run is an item wizard). When running a project wizard, both parameters are null.

This code really does two things. First, it pulls a number of variable values from the wizard using wizard.FindSymbol. (The wizard variable is added to the script's context by the wizard engine so that the script can access the engine in order to do its job. The engine also makes a dte variable available, which refers to the VS.NET automation object.) The second thing it does is to use these values to create the new project by calling the utility function CreateCSharpProject. (This is defined in the shared C# common.js, as are all of the other utility functions used in this example.)

Having created the project, the next step is the call to CreateInfFile. This utility function parses the wizard's template.inf, processing any template directives, creating a temporary file containing the results. (This means that the template.inf file can contain template directives, which allows the set of files that a wizard creates to be determined dynamically. Without this step, a wizard would always end up adding the same set of files with the same names.)

Once the temporary templates.inf file is created, the script then needs to tell the wizard engine to process all of the files listed in this .inf file. C# wizards usually do this by calling the AddFilesToCSharpProject utility function. This parses the processed .inf file, and for each file listed there, it processes any template directives and adds a file containing the processed results to the project.

9.2.2 Template Directives

The template syntax is very simple: square brackets with an exclamation mark after the opening bracket—[! ...]—denote a template directive. The six keywords that you can use within a template directive are shown in Table 9-4.

Table 9-4. Template directive syntax

Keyword

Description/usage

[!if SYMBOL_NAME]

Checks the value of the Boolean symbol SYMBOL_NAME. If the value is true, the following block is included. If not, it is omitted.

[!else]

Part of the [!if] control structure. Allows an alternate block to be included when the condition is false.

[!endif]

End of the [!if] block.

[!output SYMBOL_NAME]

or

[!output "string"]

If a symbol name is provided, the value of the symbol will be sent to the output stream. If a literal string is supplied, the string will be sent instead.

[!loop = SYMBOL_NAME]

or

[!loop = number]

The block following this directive will be repeated as many times as specified by either the named numeric symbol's value or by the numeric constant.

[!endloop]

Marks the end of a loop block.

9.2.3 Copying and Modifying an Existing Wizard

If you want a wizard that is very similar to an existing wizard, it does not make sense to build a new wizard from scratch—it is much easier to adapt an existing wizard to your needs. We will now walk through the process of copying and modifying an existing wizard. In this example, we will define a modified C# Class Library project that creates an assembly with a strong name. (The default C# Class Library project does add the appropriate attributes to do this in the AssemblyInfo.cs file, but it leaves them blank.)

Since it is easy to build new wizards by copying the existing wizards, there is never any need to modify the built-in ones. You should avoid the temptation to change the built-in wizards, since your changes may be overwritten when you install a VS.NET service pack. (VS.NET does not expect you to change its files, so it reserves the right to replace them when a service pack is installed.)

Reusing the C# Class Library project template is a fairly simple task. First, we must make a copy of the wizard's files. You can find these in the VC#\VC#Wizards\CSharpDllWiz folder in the Visual Studio .NET installation directory. Copy the files into a new directory called CSharpSNDLLWiz (SN for strong name), also under the VC#\VC#Wizards directory—this is where the VS.NET wizard engine expects all C# wizard directories to live.

We will need to modify the files in the Templates directory a little to make the template meet our needs. The project will have the same basic structure—it will contain an AssemblyInfo.cs file and an initial class file, so the template.inf file will not need modifying. The default class definition will also be just fine, so you can leave that as it is. Only the AssemblyInfo.cs template needs to be changed.

The AssemblyInfo.cs file is the usual place for all the assembly-level attributes. This is where the attribute that indicates the location of the strong name key file should go. This filename will be generated when the wizard is run—it will be placed in a variable that will be filled in by code in the default.js file. We need to modify the AssemblyInfo.cs to include an [!output] directive that will put this key file name into the source code. The AssemblyInfo.cs file already contains a line with an empty AssemblyKeyFile attribute, but we will modify it thus:

[assembly: AssemblyKeyFile("[!output KEY_FILE_NAME]")]

This is the only change we will make to the template files. But for this modified AssemblyInfo.cs template to work, we will need to change the default.js script file. It must do three things:

  • Execute the sn.exe utility to create a strong name key file (.snk)

  • Add a symbol called KEY_FILE_NAME containing the name of the key file, so that the AssemblyInfo.cs template will work correctly

  • Add the .snk file to the project

    Adding the .snk file to the project is not a strict requirement. We are doing it here for ease of use because it can then be added to source control. However, if you are relying on the secrecy of your strong name's private key to guarantee the authenticity of your code, you will probably want to use a more robust key management strategy than checking the key pair in to source control where everyone can see it. The technique shown here is appropriate only if you require the uniqueness offered by a strong name but don't care about its code signing capabilities.

Because default.js executes before the project directory is actually created, we must create the .snk file in a temporary directory, then tell the project object to add it to the project. This will cause VS.NET to copy the file to the appropriate place once the project directory has been created.

We can use the shell's Tools.Shell command to invoke the sn.exe command-line utility. (We pass sn.exe the -k switch to indicate that we would like it to generate a new key file. We also pass in the path and name of the file in which to create the key.) Because the dte object's ExecuteCommand method returns before the sn.exe command finishes executing, we have to poll to see if the file has been created before adding it to the project. The code for all this is shown in Example 9-11.

Example 9-11. Creating a strong name in a wizard
function CreateSNKeyFile(project, projectname)
{
    var fso;
    fso = new ActiveXObject("Scripting.FileSystemObject");
    var TemporaryFolder = 2;
    var tfolder = fso.GetSpecialFolder(TemporaryFolder);
    var strTempFolder = fso.GetAbsolutePathName(tfolder.Path);
    var keyfile = strTempFolder + "\\" + projectname + ".snk";
    var exestring = "sn -k " + keyfile;
    dte.ExecuteCommand("Tools.Shell",exestring);
    // Wait for the file to be created.
    while(!fso.FileExists(keyfile))
    {
    }
    // Add the symbol with the appropriate path onto the filename
    wizard.AddSymbol("KEY_FILE_NAME","..\\\\..\\\\" +
        projectname + ".snk");
   
    // Add the file to the project.
    var projfile = project.ProjectItems.AddFromTemplate(keyfile,
        projectname + ".snk");
   
}

We need to call this function from OnFinish, of course, but other than that, no further changes need to be made to the wizard files. However, every wizard must have a corresponding .vsz file. Because this is a C# project wizard, this file must go in the VC#\CSharpProjects folder. Fortunately, we can just copy the existing CSharpDLL.vsz file in that folder into a new file called CSharpSNDLL.vsz. The only change we need to make to this file is to set the name of the wizard to CSharpSNDLL:

VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.7.1
Param="WIZARD_NAME = CSharpSNDLLWiz"
Param="WIZARD_UI = FALSE"
Param="PROJECT_TYPE = CSPROJ"

Finally, we must tell VS.NET how we would like this template to appear in the New Project dialog—remember that VS.NET looks for this information in .vsdir files. You could just open the CSharp.vsdir file in the VC#\SharpProjects directory and copy the CSharpDLL line and put it at the end of the file. However, since VS.NET is happy to load any number of .vsdir files, there is no need to go editing VS.NET's own files. So, instead, we will create a new file called MyProjects.vsdir. This file will contain just one line—a copy of the CSharpDLL line from the CSharp.vsdir file, but with the first value changed to point to the new .vsz file and the third and fifth values changed from resource IDs to text to give our wizard a distinctive name and description, as shown in Example 9-12. (Note that this has been split across multiple lines to make it fit—the actual file contains just a single line.)

Example 9-12. The new wizard's .vsdir file
CSharpSNDLL.vsz|0|SN Class Library|20|
Strongly-named class library|
{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}|4547|0|IanProject

When you create a new SN Class Library project, the wizard will run, generating a new key file and adding the appropriate filename into the AssemblyInfo.cs file.

    Team LiB   Previous Section   Next Section