9.2 The VS.NET Wizard EngineAll 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.
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.
9.2.1 Wizard ExecutionAll 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 UIWizards 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.
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 templatesEvery 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 directivesusing 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.
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 methodfunction 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 DirectivesThe 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.
9.2.3 Copying and Modifying an Existing WizardIf 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.)
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:
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 wizardfunction 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 fileCSharpSNDLL.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. |