9.3 Custom Wizard EnginesWe are not obliged to use the wizard engine in our custom wizards. Instead, we can write a COM server, implementing the IDTWizard interface (the same interface that the wizard engine implements) and have more control over the way the wizard looks and works. There are two reasons you might choose to write a custom IDTWizard implementation instead of using the wizard engine infrastructure. One is that you can write your wizard in the language of your choice (either managed or unmanaged), rather than being forced to use JScript. The second reason is that, if your wizard has a UI, you get total control over it—you are not limited to using HTML. The downside of writing the custom implementation is that you can no longer use the convenient template directive syntax—you are responsible for generating any files yourself. However, if you plan to support both VB.NET and C#, this may well not be a problem: if you use the classes in the System.CodeDom namespace, you can write a single code generator that can create source code in either language, removing the need to have two different sets of template files for each language.
As an example, let's build a wizard that creates a new file containing a class with a skeleton implementation of the IDTWizard interface. (So we will write a wizard wizard, so to speak.) We will use the CodeDom API to generate the necessary source files. The CodeDom is a .NET API, so we will write this wizard in C#. (But once we've written it, it will be able to generate new VB projects as well as new C# projects.) Besides implementing the IDTWizard interface, the class that our wizard generates will need to support COM registration. For a .NET project, this means the containing assembly must be signed using a strong name, as well as have the appropriate attributes to support COM registration. This wizard is aiming only to add new classes to an existing project, so we can use the SN Class Library wizard we wrote earlier to create a project that will be signed with a strong name. This new wizard will just have to add a Guid attribute to the class it creates. The IDTWizard interface has only one method, Execute. The first argument to Execute is a reference to the DTE object (the top-level object in the VS.NET object model). The ContextParams parameter is an array, the contents of which depend on whether this wizard is executing as a project wizard or an item wizard. Table 9-5 shows what is passed in each case.
The third parameter, CustomParams, is a collection of the Param elements from the .vsz file. For each Param=<Value> line in the .vsz file of the wizard, there will be a string in the CustomParams array. These strings are in the same format as they appear in the .vsz file. So, with the .vsz file shown in Example 9-13, there would be two entries: "MY_PARAM = Foo", and "This is another parameter". Example 9-13. Custom parameters in a .vsz fileVSWIZARD 7.0 Wizard=MyWizard.CustomWizardEngine Param="MY_PARAM = Foo" Param="This is another parameter" The last parameter of the Execute method is the logical return value—it indicates the outcome of the wizard. It must be set to one of the enumerated values listed in Table 9-6.
Our Execute method must perform the following steps:
Example 9-14 shows the implementation of Execute. It starts by caching the reference to the Project object in a member variable. Next, it switches code path based upon the language of the project. (If the project is a language other than VB.NET or C#, the wizard raises an error message.) Example 9-14. IDTWizard implementationpublic void Execute(object Application, int hwndOwner, ref object[ ] ContextParams, ref object[ ] CustomParams, ref EnvDTE.wizardResult retval) { // The third item in ContextParams is the pProjectIitems object. ProjectItems pi = (ProjectItems)ContextParams[2]; // Get the project from the ProjectItems reference. project = pi.ContainingProject; // We use the CodeModel to find out which language is in use. CodeModel cm = project.CodeModel; retval = EnvDTE.wizardResult.wizardResultSuccess; switch(cm.Language) { // Switch based upon language of project. case CodeModelLanguageConstants.vsCMLanguageCSharp: DoCSharp( ); break; case CodeModelLanguageConstants.vsCMLanguageVB: DoVB( ); break; default: MessageBox.Show("This wizard can only be used from " "C# or VB.NET projects"); retval = EnvDTE.wizardResult.wizardResultFailure; return; } } The CodeDom defines an abstract API for generating code. This means that the majority of our code generation will be common for the VB.NET and C# code paths. Our example wizard has a class called CodeGen that provides a generic implementation for generating the code file using any CodeDomProvider, shown in Example 9-15. Example 9-15. Code generation with CodeDomclass CodeGen { public static string Generate(string filename, string extension, string classname, string nspace, string progid, CodeDomProvider cdp) { // Create a code generator. ICodeGenerator cg = cdp.CreateGenerator( ); // Need two namespaces so that the namespace imports appear // in the "outer" namespace that doesn't have a name. System.CodeDom.CodeNamespace cnamespace2 = new System.CodeDom.CodeNamespace( ); System.CodeDom.CodeNamespace cnamespace = new System.CodeDom.CodeNamespace(nspace); // Add the approprate imports. cnamespace2.Imports.Add(new CodeNamespaceImport("System") ); // The vs.net object model. cnamespace2.Imports.Add(new CodeNamespaceImport("EnvDTE") ); // Needed for the COM interop attributes. cnamespace2.Imports.Add( new CodeNamespaceImport("System.Runtime.InteropServices")); // If this is VB--add the VisualBasic namespace. if(cdp.GetType( )= =typeof(VBCodeProvider)) cnamespace2.Imports.Add( new CodeNamespaceImport("Microsoft.VisualBasic")); // Create the new class. CodeTypeDeclaration co = new CodeTypeDeclaration (classname); // Add the Guid attribute. Guid g = Guid.NewGuid( ); co.CustomAttributes.Add( new CodeAttributeDeclaration("Guid", new CodeAttributeArgument( new CodePrimitiveExpression(g.ToString("D"))))) ; // Implement IDTWizard. (Must also add Object base type // for VB.NET, otherwise the VBCodeDom uses "Inherits IDTWizard" // instead of "Implements IDTWizard".) CodeTypeReference ctr = new CodeTypeReference(typeof (EnvDTE.IDTWizard)); co.BaseTypes.Add(typeof(object)); co.BaseTypes.Add( ctr); // Add the type to the namespace. cnamespace.Types.Add (co); // Add the Execute method. CodeMemberMethod cm = new CodeMemberMethod( ); cm.Name = "Execute"; cm.PrivateImplementationType = ctr; cm.Attributes = MemberAttributes.Public | MemberAttributes.Final ; // Add parameters. cm.Parameters.Add ( new CodeParameterDeclarationExpression(typeof(object), "Application")); cm.Parameters.Add ( new CodeParameterDeclarationExpression(typeof(int), "hwnd")); CodeParameterDeclarationExpression cp = new CodeParameterDeclarationExpression(typeof(object[ ]), "ContextParams"); cp.Direction=FieldDirection.Ref; cm.Parameters.Add (cp); cp = new CodeParameterDeclarationExpression(typeof(object[ ]), "CustomParams"); cp.Direction = FieldDirection.Ref; cm.Parameters.Add(cp); cp = new CodeParameterDeclarationExpression( typeof(EnvDTE.wizardResult), "retval"); cp.Direction = FieldDirection.Ref; cm.Parameters.Add(cp); // Add the method to the type. co.Members.Add (cm); // Create the text file. using (TextWriter w = new StreamWriter(fullFileName, false)); { //Generate the code. cg.GenerateCodeFromNamespace( cnamespace2,w,null); cg.GenerateCodeFromNamespace (cnamespace, w, null); } return fullFileName; } } Recall that our IDTWizard implementation in Example 9-14 called one of two functions depending on the project language. With the code generation class in Example 9-15 in place, all these two functions need to do is create an instance of the appropriate CodeDomProvider: private void DoCSharp( ) { CSharpCodeProvider cdp = new CSharpCodeProvider( ); InternalExecute(cdp, ".cs"); } private void DoVB( ) { VBCodeProvider cdp = new VBCodeProvider( ); InternalExecute(cdp, ".vb"); } The rest of the wizard code is shown in Example 9-16. It is dedicated to displaying the UI, executing the CodeDom class, adding the file to the project, and adding a reference to envdte.dll to the project. (This UI is a simple Windows Forms dialog that asks the user for a class name and a namespace. It contains no code of direct relevance to writing wizards, so it is not shown here.) Example 9-16. Finishing off the wizardprivate void GetInputs( ) { // Display the form to retrieve user settings. wf = new WizardForm( ); wf.ShowDialog( ); wf.Dispose( ); } private void InternalExecute(CodeDomProvider codeDom, string ext) { GetInputs( ); // Get a temp directory. TempFileCollection tfc = new TempFileCollection( ); string cn = wf.classNameTextBox.Text; string ns = wf.namespaceTextBox.Text; string filepath = tfc.BasePath + wf.classNameTextBox.Text; string progid = wf.progIdTextBox.Text; // Execute the code generation method. string filefull = CodeGen.Generate(filepath, ext, cn, ns, progid, codeDom); // Add generated file to project. AddFiletoProject(filefull,cn+ext); } private void AddFiletoProject(string filename,string realname) { // Add a reference to envdte.dll. VSProject vsp = (VSProject)project.Object; vsp.References.Add("envdte"); // Add the file to the project. project.ProjectItems.AddFromTemplate(filename,realname); } One last step is required to make this wizard work: we need to add an entry to the appropriate .vsdir file(s) and add the corresponding .vsz file to the appropriate directory. Since this is an item wizard, the .vsz file will go the item directories for both VB and C#—VC#\CSharpProjectItems and Vb7\VBProjectItems, respectively. It may seem a bit wasteful to have two copies, and although we could try and use long relative paths in the .vsdir files, the .vsz is so simple that it's not really worth the effort. The .vsz just needs to have the correct ProgID for the class: VSWIZARD 7.0 Wizard=CustomWizardWizard.Wizard |