Windows DevCenter    
 Published on Windows DevCenter (http://www.windowsdevcenter.com/)
 See this if you're having trouble printing code examples


Developing Visual Studio Project Wizards

by Ron Petrusha
06/06/2007

While creating customized Visual Studio project templates alone can be useful in providing some boilerplate code and in eliminating the need to perform some repetitive operations (such as adding references to .NET assemblies), the real power of Visual Studio project templates becomes evident when they are combined with Visual Studio wizards. When used with a wizard, a Visual Studio template can create a project whose content reflects conditions specified by the template user at design time.

Visual Studio 2005 features a new object model for developing wizards, as well as a new method of integrating wizards with templates. Unfortunately, in the online help included with Visual Studio .NET, the documentation for its previous versions remains very prominent, while the new documentation is difficult to find. In addition, although large portions of the object model for developing wizards in previous versions still work in Visual Studio 2005, other portions do not.

The Wizard Object Model

Wizards that extend Visual Studio 2005 templates are developed as class library projects. The Visual Studio 2005 extensibility model requires that the class that provides the implementation code implement the IWizard interface. The methods of IWizard are automatically called by Visual Studio at various points as it creates a project from your template. These callback methods give you access to the Visual Studio object environment--which is represented by the DTE2 interface, which in turn represents the application-level object in the object model--and to the VSProject interface, which provides access to elements of the project created by the template. Since IWizard is the central element of the object model, we'll begin by examining it in detail.

The IWizard Interface

The IWizard interface, which is defined in the Microsoft.VisualStudio.TemplateWizard namespace and is found in the Microsoft.VisualStudio.TemplateWizard.dll assembly, defines six callback methods that are invoked by Visual Studio as it generates a project or a project item from a template. Each of these six methods must be implemented in your code, even if you only provide a stub implementation (that is, if you provide a method definition but don't actually include any code). The six callback methods are defined in the following subsections.

The RunStarted Method

This method is called by Visual Studio whenever any template that includes a wizard is first started. The RunStarted method serves as the entry point into wizard code. The method has the following signature:

RunStarted(automationObject As Object, _
           replacementsDictionary As Dictionary(Of String, String), _
           runKind As WizardRunKind, _
           customParams As Object() )

Its parameters reflect the items of information that Visual Studio provides about the Visual Studio environment, the project, and the template. These are:

automationObject

An argument of type Object that represents the Visual Studio application. It in turn is cast or converted to an EnvDTE80.DTE2 object, the application-level object in the Visual Studio extensibility object model, and is typically assigned to a global variable. For example, the following Visual Basic code provides a skeletal framework for caching the reference to the Visual Studio application object:

Public Class MyWizard : Implements IWizard
   Private vsApp As EnvDTE80.DTE2

   Public Sub RunStarted(ByVal automationObject As Object_
                         ByRef replacementsDictionary As Dictionary(Of String, String), _
                         ByVal runKind As WizardRunKind, _
                         ByVal customParams As Object) _
              Implements IWizard.RunStarted
      If TypeOf automationObject Is DTE2 Then
         Me.vsApp = DirectCast(automationObject, DTE2)
      Else
         Throw New ArgumentException("Unable to get a reference to the Visual Studio environment. ")
      End If
   End Sub
End Class

The following is the equivalent C# code:

public class MyWizard : IWizard
{
   private DTE2 vsApp;

   public void RunStarted(object automationObject, 
                          Dictionary<string, string> replacementsDictionary,
                          WizardRunKind runKind, 
                          object[] customParams)
   {
      if (automationObject is DTE2)
         this.vsApp = (DTE2)automationObject;
      else
      throw new ArgumentException("Unable to get a reference to the Visual Studio environment. ")
   }
}
replacementsDictionary

The replacements dictionary defines the replaceable string parameters used for a project. (For details on replaceable string parameters, see Creating Visual Studio Project Templates.) The replacementsDictionary parameter makes these replaceable string parameters programmatically available to the wizard, and allows the wizard to modify the value of existing parameters, as well as to add new items to the dictionary.

The replacements dictionary is available to the RunStarted method as a generic Dictionary object whose keys and values are both strings. The key consists of the parameter name enclosed in dollar-sign symbols (such as $projectname$). You can iterate the dictionary using Visual Basic code like the following:

For Each entry As KeyValuePair(Of String, String) In replacementsDictionary
   ' Do something with the key/value pair. 
   ' Its key is returned by the entry.Key property.
   ' Its value is returned by the entry.Value property
Next
or using C# code like the following:
foreach (KeyValuePair<string, string> entry in replacementsDictionary)
{
   // Do something with the key/value pair. 
   // Its key is returned by the entry.Key property.
   // Its value is returned by the entry.Value property
}

You can also retrieve a value by its key name by using the Dictionary object's Item property (in Visual Basic) or its indexer (in C#). However, the attempt to retrieve a key that is not defined in the dictionary throws a KeyNotFoundException. To prevent this, you call the Dictionary object's ContainsKey method beforehand to verify that the key exists.

runKind

Indicates how the template was invoked. A value of WizardRunKind.AsNewProject indicates that the template was invoked to create a new project. A value of WizardRunKind.AsNewItem indicates that the template was invoked to create a new project item. The runKind parameter allows a single wizard to be used in templates that create new projects as well as new project items.

customParams

The customParams array was probably intended to contain any custom parameters defined by the template. Custom parameters are defined by the <CustomParameters> section in a .vstemplate file's <TemplateContent> section. Each custom parameter is defined using the <CustomParameter> tag, which takes the form:

<CustomParameter Name="parameterName" Value="parameterValue" />

However, custom parameters are added to the generic Dictionary object available from the replacementsDictionary parameter. The customParams parameter can be safely ignored.

Custom parameters can be used, for example, to allow a single wizard that modifies code in a project to be used with both a Visual Basic and a C# template. The Visual Basic version of the template could include the following definition in its .vstemplate file:

<CustomParameter Name="language" Value="VisualBasic" />

The C# template could include the following definition:

<CustomParameter Name="language" Value="CSharp" />

The ShouldAddProjectItem Method

This method is called when a wizard is launched by a project item template before the item is added to a project. It is not called when a wizard is launched from a project template. The method has the following signature:

ShouldAddProjectItem(filePath As String) As Boolean

where filePath is the name of the file to be added. Its path is included only if the file is to be added to a subdirectory of the project directory. The method should return True if the item is to be added to the project; otherwise, it should return False. If the method returns False, Visual Studio gracefully handles the failure to add the item.

It is important, however, that none of the code executed in the RunStarted method make modifications to the project based on the assumption that the project item will be added.

The ProjectFinishedGenerating Method

This method is called when a wizard invoked by a project template has finished generating the project. (Its equivalent method for project item templates is the ProjectItemFinishedGenerating method.) When the method is called, Visual Studio has already replaced all replaceable string parameters found in the project's source code with their values and has added all files defined by the template to the project. The method signature is:

ProjectFinishedGenerating(project As Project)

where project is an EnvDTE.Project interface object that provides access to the project.

The most common uses of this method are to add some additional files to or remove some existing files from the project, to add references to the project, or to modify the source code in the project. When the method is called, Visual Studio has already replaced all replaceable string parameters found in the item's source code with their values.

The ProjectItemFinishedGenerating Method

This method is called when a wizard invoked by a project item template has finished generating the item. (Its equivalent method for project templates is the ProjectFinishedGenerating method.) The method is invoked only if the item is added to the project--that is, if the ShouldAddProjectItem method returns True. Its signature is:

ProjectItemFinishedGenerating(ByVal projectItem As EnvDTE.ProjectItem)

where projectItem is a reference to the item to be added to the project.

The BeforeOpeningFile Method

The presence of the OpenOrder attribute in the <ProjectItem> tag in a template's .vstemplate file determines whether or not that project item is to be opened in the Visual Studio environment once Visual Studio has finished generating the project. This method is called for wizards invoked by both project templates and by project item templates before Visual Studio actually opens each file. Its signature is:

BeforeOpeningFile(projectItem As ProjectItem)

where projectItem is an EnvDTE.ProjectItem interface object containing a reference to the project item that is about to be opened.

The RunFinished Method

This method is called to indicate that Visual Studio has finished processing the project or project item template. The method has no parameters.

The DTE and DTE2 Interface Objects

The EnvDTE80.DTE2 object represents the Visual Studio application. It is derived from the DTE object, which represented the Visual Studio application in the initial release of Visual Studio .NET. The RunStarted method passes a reference to the Visual Studio application object to wizards invoked from both project templates and project item templates. Access to the DTE2 object requires that you add references to the envdte.dl and EnvDTE80.dll assemblies.

The DTE object was developed for creating extensions for Visual Studio .NET 1.0. the DTE2 object was developed for creating extensions for Visual Studio .NET 2003. You should be aware that in Visual Studio 2005, some of the members in the object model are no longer implemented. Although the members remain for reasons of compatibility, they no longer behave as previously documented.

The DTE2 object exposes much of the functionality of the Visual Studio design time environment, and therefore is the top-level object in a rather sizable object model. For wizard development, however, the following members of the DTE2 object are most important:

The DTE2 object also includes a GetThemeColor method, which takes a vsThemeColors argument representing a particular user interface element and returns a UInt32 value representing the color of that element. The method promises to make it possible to display a wizard's user interface in colors that are compatible with those used by Visual Studio. Unfortunately, however, the method appears to not be implemented in Visual Studio 2005. It returns the same value (0xFF000000) regardless of the vsThemeColors value passed to it.

The Project Interface Object

The EnvDTE.Project interface represents the Visual Studio project. Visual Studio passes a reference to the Project created by a template to the ProjectFinishedGenerating method. In addition, if your wizard needs to work with the Project object before it is passed a reference to it, it can retrieve the Project object by name from the Projects collection of the Solution object returned by the DTE2.Solution property. The following members of the Project object are especially important in wizard development:

The ProjectItem Interface Object

The EnvDTE.ProjectItem interface represents a Visual Studio project item. Visual Studio passes a reference to the ProjectItem interface object that represents the item that a template adds to a project in the ProjectItemFinishedGenerating method. It also passes a reference to ProjectItem objects representing each of the items it is to open in the BeforeOpeningFile method. Finally, all of a project's items are accessible through the EnvDTE.Project object's ProjectItems property. It includes the following members:

The VSProject Interface Object

The VSProject interface, which is new to the Visual Studio 2005 extensibility model, represents a Visual Basic, Visual C#, or Visual J# project in the Visual Studio environment. It provides a project-level object model that you might otherwise expect to find in the EnvDTE.Project interface. The VSProject object and its child objects are defined in the VSLangProj namespace and require that a reference to the valangproj.dll assembly be added to your wizard development project.

To instantiate a VSProject object, you convert or cast the EnvDTE.Project object passed to the ProjectFinishedGenerating method to a VSProject object. This requires a C# statement like the following in the code for the ProjectFinishedGenerating method:

VSProject vsproject = (VSProject) project.Object;

The equivalent Visual Basic statement is:

Dim vsproject As VSProject = DirectCast(project.Object, VSProject)

The VSProject object includes the following members:

Handling Abnormal Termination

Particularly if your wizard displays a user interface, you have to be prepared for the possibility that the user might cancel your wizard or fail to provide some information necessary for the wizard to continue. In this case, you want Visual Studio to gracefully exit from the wizard, rather than to throw an exception to the wizard user.

Interestingly, the wizard communicates with Visual Studio by throwing either of two exceptions, both of which are defined in the Microsoft.VisualStudio.TemplateWizard namespace. (They're found in the Microsoft.VisualStudio.TemplateWizardInterface.dll assembly, the same assembly that contains the IWizard interface.) The first, WizardCancelledException, indicates that the wizard has been cancelled before it is completed. The second, WizardBackoutException, indicates that the wizard is to be backed out before it has been completed. These exceptions are unusual insofar as your wizard throws the exception, and Visual Studio itself provides the exception handling code that handles the exception.

WizardCancelledException

When Visual Studio handles the WizardCancelledException, it aborts the execution of the wizard, stops loading the template that activated the wizard in the first place, and, if possible, returns the Visual Studio environment to the state it was in before the template was selected.

Making effective use of the WizardCancelledException in your application requires that you be aware of several undocumented (or at least unmentioned) features:

This latter restriction poses something of a dilemma. The most common reason for cancelling a wizard is that the user has clicked the Cancel button on one of the wizard's dialogs, indicating that he or she no longer wishes to continue with the wizard or the project. Code such as the following (which illustrates how not to throw a WizardCancelledException) therefore seems almost intuitive:

Public Class ProjectWizard : Implements IWizard
   Public Sub RunStarted(ByVal automationObject As Object, _
                         ByVal replacementsDictionary As Dictionary(Of String, String), _
                         ByVal runKind As WizardRunKind, _
                         ByVal customParams() As Object) _
                         Implements IWizard.RunStarted
      ' Initialization code goes here
      Dim frm As New WizardForm()
      frm.ShowDialog()
      ' handling of form data goes here 
   End Sub

   ' Other implemented IWizard methods
End Class

Public Class WizardForm : Inherits Form
   ' Form code to display interact with user

   Public Sub CancelButton_Click(ByVal sender As Object, _
                                 ByVal e As EventArgs) _
                                 Handles CancelButton.Click
      ' This exception is ignored
      Throw New WizardCancelledException("The user has cancelled the wizard.")
   End Sub
End Class

In this case, however, Visual Studio completely ignores the exception, the wizard continues to execute, and Visual Studio attempts to either create a new project (if the user selected a project template) or create a new project item (if the user selected a project item template). This code fails because the exception is thrown in the Form class, and not the class that implements IWizard.

Instead, if the wizard is to be cancelled as a result of the user selecting the Cancel button in a form, the button's DialogResult property should be set to Cancel, so that the form's DialogResult property will be set to DialogResult.Cancel if the form closes as a result of the user pressing the Cancel button. Code in the class implementing the IWizard interface can then evaluate the value of the form's DialogResult property and, if it is DialogResult.Cancel, throw the WizardCancelledException. The skeletal Visual Basic code appears as follows:

Public Class ProjectWizard : Implements IWizard
   Public Sub RunStarted(ByVal automationObject As Object, _
                         ByVal replacementsDictionary As Dictionary(Of String, String), _
                         ByVal runKind As WizardRunKind, _
                         ByVal customParams() As Object) _
                         Implements IWizard.RunStarted
      ' Initialization code goes here
      Dim frm As New WizardForm()
      frm.ShowDialog()
      If frm.DialogResult = DialogResult.Cancel Then
         Throw New WizardCancelledException("The wizard has been cancelled by the user.")
      End If
      ' handling of form data goes here 
   End Sub

   ' Other implemented IWizard methods
End Class

The equivalent C# code is:

public class ProjectWizard : IWizard
{
   public void RunStarted(object automationObject, 
                          Dictionary<string, string> replacementsDictionary, 
                          WizardRunKind runKind, 
                          object[] customParams)
   {
      // Initialization code goes here
      Form frm = new WizardForm();
      frm.ShowDialog();
      if (frm.DialogResult == DialogResult.Cancel)
         throw new WizardCancelledException("The wizard has been cancelled by the user.");

      // handling of form data goes here
   }
}

Although the WizardCancelledException is most commonly thrown in the RunStarted method, it can be thrown in any of the IWizard implementation methods. Visual Studio will terminate the wizard, abort the process of loading the template, and, if possible, restore the Visual Studio environment to its state before the new project or project item template was loaded.

WizardBackoutException

Visual Studio responds to the WizardBackoutException very much like it does to the WizardCancelledException: it aborts the execution of the wizard, stops loading the template that activated the wizard in the first place, and, if possible, returns the Visual Studio environment to the state it was in before the template was selected. It then reopens the dialog (either the New Project or Add New Item dialog) from which the template whose wizard was just terminated was selected. Also like the WizardCancelledException, the WizardBackoutException does not itself provide any user interface, and the exception is completely ignored if it is thrown from outside of the class that provides the IWizard implementation.

Integrating the Template and Wizard

Visual Studio 2005 also features a new (and more straightforward) method of integrating a Visual Studio template with its wizard. The wizard assembly itself must be signed and registered in .NET Global Assembly Cache, or GAC. This allows the assembly to be located regardless of where it resides in the file system. The wizard is then identified by its strong name and its fully qualified type name in the .vstemplate file of its associated template.

Signing the Wizard Assembly

A signed assembly is one that has been compiled with a public key, which provides it with a unique identify. All signed assemblies can be identified by strong name in addition to friendly name. (We'll see how to determine an assembly's strong name in the section, "Modifying the .vstemplate File."

Visual Studio 2005 makes the process of generating a signed assembly extremely easy. To sign an assembly, open the wizard project's Properties dialog and select the Signing tab, as shown in Figure 1. Check the "Sign the Assembly" box, and use the "Choose a strong name key file" combo box to either navigate to an existing key file, or have Visual Studio generate a new key file.


Figure 1. The Signing tab of the Project Properties dialog

Registering the Wizard Assembly in the GAC

Once you've compiled the assembly containing the wizard, you must register it in the GAC. To do this, you use gacutil.exe, the Global Assembly Cache utility. To do this, open a console window and navigate to the directory contains the wizard assembly. Next, assuming that the path to GACUTIL is in your PATH environment variable, type in the command:

GacUtil /i <wizard_assembly_name>.dll

where wizard_assembly_name is the root filename of your wizard assembly.

You can have Visual Studio take care of registering your assembly in the GAC as part of the build process. To this, you just have to provide the command line for a post-build event. Visual Studio executes it after it has attempted to build your project.

To have Visual Studio automatically register your wizard assembly after a successful build:

  1. Open the wizard project's Properties dialog, and select the Compile tab.
  2. Click the Build Events button at the bottom of the tab page.
  3. Make sure that On successful build is selected in the Run the post-build event drop-down list box.
  4. In the Post-build event command line text box, enter code like the following:
  5. gacutil /u <assembly_name>
    gacutil /I <assembly_name>.dll
    where <assembly_name> is the root name of the wizard assembly.

If the path to GACUTIL is not in the PATH environment variable, you'll have to add a fully qualified path. Also, the script works on the current directory, which is the directory in which the compiled assembly is stored. If you want to copy the assembly to some other location, you should change to that directory before registering the new version of the assembly in the GAC.

Note that this script unregisters the existing version of the assembly before registering the one just compiled. You should always unregister the existing version of the assembly before registering the new version. This prevents the .NET runtime from working with a cached version of your assembly that doesn't necessarily reflect the latest changes. (Needless to say, forgetting to do this makes debugging an interesting experience.)

Modifying the .vstemplate File

Now that your assembly is signed and registered, you just have to link it to the template whose selection activates it. To do this, you modify the .vstemplate file that Visual Studio creates when you export a template.

When Visual Studio exports a template file, it creates a ZIP file that contains all of the template's files. The format of this ZIP file, however, is different than the standard ZIP file format, which means that, while you can view and extract the files in the Visual Studio-generated ZIP file, you can't replace them. In other words, to integrate a wizard with a template, you'll have to extract the files from the existing template file, modify the .vstemplate file, and save the files to a new ZIP file.

The modification required to the .vstemplate file is itself quite simple. The .vstemplate file is an XML file. At the end of the template file, but before the closing </VSTemplate> tag, add a <WizardExtension> section. The <WizardExtension> section contains two tags that define the wizard. The first, the <Assembly> tag, provides the strong name of the wizard assembly. The second, the <FullClassName> tag, provides the fully qualified type name of the wizard class (that is, of the class that implements IWizard). You can get the strong name of a wizard assembly by opening a console window and (again assuming that the path to GACUTIL is in your PATH environment variable) entering the following command:

GacUtil /l <wizard_assembly_name>

where wizard_assembly_name is the name of the assembly, without its .dll extension. In Figure 2, for example, GacUtil is used to display the strong name of an assembly named WizardVB1.


Figure 2. Getting the strong name of an assembly

The finished definition of a wizard in a .vstemplate file might look something like the following:

...
   </TemplateContent>
   <WizardExtension>
      <Assembly>wizardvb1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2e58463dcf7e9cb7</Assembly>
      <FullClassName>CustomWizards.Wizard1</FullClassName>
   </WizardExtension>
</VSTemplate>

A Simple Example

Let's look at a very simple example that suggests some of the potential of templates. We have an class library for application development that has two versions in two different assemblies, AppLib and AppLib2. Each version supports a different kind of application. Version 1 is for building console applications. The classes in Version 2, which derive from and call methods in the Version 1 library, support Windows applications. We want a project type that allows developers to specify which version of the class library they wish to use, and that handles adding the necessary references and making the initial call to a library method.

The Class Libraries

To illustrate, we can create a very simple assembly that displays a message to the console. Its Visual Basic source code is:

Namespace MyCompanyApp
   Public Class AppLib
      Public Shared Sub DisplayGreeting()
         Console.WriteLine(GetMessage())
      End Sub

      Public Shared Function GetMessage() As String
         Return "Welcome!"
      End Function
   End Class
End Namespace
The version 2 library, written in C#, is even simpler:
using MyCompanyApp;
using System;
using System.Windows.Forms;

namespace MyCompanyApp
{
   public class AppLib2 : AppLib
   {
      public static void DisplayGreetingDialog()
      {
         MessageBox.Show(GetMessage());
      }
   } 
}

Both assemblies should be compiled and placed in the GAC.

Creating the Template

Next, create the template that will be used to generate individual projects. To do this, create a console mode application in either C# or Visual Basic. Add a reference to AppLib.dll to the project. Rename the code file to AppClass.cs or AppClass.vb, and replace the contents of the C# code file with the following code:

using MyCompanyApp;
using System;

namespace MyCompanyApp
{
   class AppClass
   {
      static void Main()
   {
      $OpeningMessage$;
   }
   }
}

The corresponding Visual Basic code is:

Imports MyCompanyApp

Module AppClass
   Sub Main()
      $OpeningMessage$
   End Sub
End Module

Notice that $OpeningMessage$ is not valid C# or Visual Basic syntax; instead, this is a replaceable string parameter. When the project is created, the wizard replaces this string with a call to the appropriate library method. The template can then be exported and its .vstemplate file modified, as discussed in the "Modifying the .vstemplate File" section. In addition, you should change the assembly reference, which appears as follows:

<Reference Include="AppLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a68e0a70a91f57a7, processorArchitecture=MSIL">
   <SpecificVersion>False</SpecificVersion>
   <HintPath>..\AppLib\bin\Release\AppLib.dll</HintPath>
</Reference>

to the following:

<Reference Include="AppLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a68e0a70a91f57a7, processorArchitecture=MSIL" />

This insures that, rather than attempting to locate a local copy in a particular directory, Visual Studio will load the assembly from the GAC. Once you've made all of the modifications, you can place the new set of files in a ZIP file and place the file in the Visual Studio project template directory for either Visual Basic or C#.

The Wizard

The wizard simply opens a Windows form that allows the developer to indicate whether the project targets version 1 or 2 of the class library. If the developer chooses version 1, the string AppLib.DisplayGreeting() replaces the replaceable string parameter. If the developer chooses version 2, AppLib.DisplayGreetingDialog() replaces the string parameter. In addition, if the developer chooses version 2, a reference to AppLib2 is added to the project. The following is the code for a class named AppWizard, which provides the IWizard implementation:

Imports EnvDTE80
Imports Microsoft.VisualStudio.TemplateWizard
Imports VSLangProj

Public Class AppWizard : Implements IWizard
   ' Declare private variables
   Private vsApp As DTE2
   Private isVB As Boolean
   Friend targetedVersion As Integer = 1

   Public Sub BeforeOpeningFile(ByVal projectItem As EnvDTE.ProjectItem) Implements Microsoft.VisualStudio.TemplateWizard.IWizard.BeforeOpeningFile

   End Sub

   Public Sub ProjectFinishedGenerating(ByVal project As EnvDTE.Project) Implements Microsoft.VisualStudio.TemplateWizard.IWizard.ProjectFinishedGenerating
      ' Add reference to AppLib2 for V2 projects
      If targetedVersion = 2 Then
         Dim appProject As VSProject = DirectCast(project.Object, VSProject)
         Dim ref As Reference = appProject.References.Add("AppLib2")
         Console.WriteLine(ref.Version)
      End If
   End Sub

   Public Sub ProjectItemFinishedGenerating(ByVal projectItem As EnvDTE.ProjectItem) Implements Microsoft.VisualStudio.TemplateWizard.IWizard.ProjectItemFinishedGenerating

   End Sub

   Public Sub RunFinished() Implements Microsoft.VisualStudio.TemplateWizard.IWizard.RunFinished

   End Sub

   Public Sub RunStarted(ByVal automationObject As Object, ByVal replacementsDictionary As System.Collections.Generic.Dictionary(Of String, String), ByVal runKind As Microsoft.VisualStudio.TemplateWizard.WizardRunKind, ByVal customParams() As Object) Implements Microsoft.VisualStudio.TemplateWizard.IWizard.RunStarted
      ' Get reference to application-level object
      Me.vsApp = DirectCast(automationObject, DTE2)

      ' Open dialog to determine version of library user develops against
      Dim frm As New WizardForm(Me)
      frm.ShowDialog()
      ' Terminate template if user has cancelled form
      If frm.DialogResult = Windows.Forms.DialogResult.Cancel Then
         ' Cancel loading of template (and wizard)
         Throw New WizardCancelledException
      Else
         ' Adjust the method call
         replacementsDictionary.Add("$rootNamespace$", "MyCompanyApp")
         If Me.targetedVersion = 1 Then
            replacementsDictionary.Add("$OpeningMessage$", "AppLib.DisplayGreeting()")
         ElseIf Me.targetedVersion = 2 Then
            replacementsDictionary.Add("$OpeningMessage$", "AppLib2.DisplayGreetingDialog()")
         ' This shouldn't happen
         Else
            Throw New WizardCancelledException("An unexpected error has caused the template to terminate.")
         End If
      End If
   End Sub

   Public Function ShouldAddProjectItem(ByVal filePath As String) As Boolean Implements Microsoft.VisualStudio.TemplateWizard.IWizard.ShouldAddProjectItem
      Return True
   End Function
End Class

The AppWizard class in turn instantiates a form class named WizardForm and passes it a reference to itself. The WizardForm class has a drop-down list box named cboVersions that displays the versions of the AppLib class library. The complete source code for the WizardForm class is:

Public Class WizardForm
   Private Const TOTAL_PAGES = 1

   Dim wizard As AppWizard

   Public Sub New(ByVal wizardClass As AppWizard)
      MyBase.New()
      InitializeComponent()
      wizard = wizardClass
   End Sub

   Private Sub WizardForm_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
      Dim ctr As Integer = 1           ' Counter to track current page (for multi-page wizards)
      Me.cboVersions.SelectedIndex = 0
   End Sub

   Private Sub btnOK_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnOK.Click
      Me.wizard.targetedVersion = Me.cboVersions.SelectedIndex + 1
   End Sub

   Private Sub brnCancel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles brnCancel.Click

   End Sub
End Class

Once you've created the wizard and registered in the GAC, and you've created the template and placed it in the appropriate directory, you can use it to create projects in Visual Studio.

Some Additional Comments

Note that we use the same wizard to handle a Visual Basic and a Visual C# project template. Ordinarily, we'd want to include some means of determining the template's target language so that we can use the individual language's syntax or address features in its Visual Studio development environment. We can do this by retrieving the string returned by the Kind property of the EnvDTE.Project interface, then looking up the value of this key in the registry. It is much easier, though, to define a custom string parameter named Language in the .vstemplate file, and to give it a value of VisualBasic for the Visual Basic template and CSharp for the C# template.

When debugging wizard applications, it's important to remember that both Visual Studio and the GAC use cached copies of their assemblies. This means that when you modify and recompile your assembly, the .NET Framework or Visual Studio may be working with a previous copy. To make sure that the current assembly is being used, it's a good practice to unregister the previous version of an assembly from the GAC and then to register the new version. The instance of Visual Studio that was used to create a template should also be closed once the template is modified, and a new instance started to work with the new one.

Ron Petrusha is the author and coauthor of many books, including "VBScript in a Nutshell."


Return to Windows DevCenter.

Copyright © 2009 O'Reilly Media, Inc.