Tag Archives: project

New New Project from Existing Code

Last time I wrote about the New Project from Existing Code wizard in Visual Studio and how we extended it to provide support for Python. This time, I’m going to look at the replacement for this dialog.

As discussed, there are a number of issues with reusing the managed languages wizard (C# and VB) for Python, largely due to the fact that it was never intended to be extensible. It is also difficult or impossible to (reliably) provide an alternate implementation, and in any case the feature is not very easy to discover. Because the usual way to create a new project normally involves skipping the menu and going straight to the list of templates, the aim was to add an item to this dialog:

New Project dialog with From Existing Python code selected

The three steps involved are creating the wizard, providing the necessary interfaces for Visual Studio and creating a template to start the wizard. The code is from changeset a8d12570c484, which was added prior to PTVS 1.5RC.

Designing a Wizard

Since this approach does not rely on reusing existing code, we were free to design the wizard in a way that flows nicely for Python developers and only exposes those options that are relevant, such as the interpreter to use with a project and the main script file. Because not every option needs to be used, and there are obviously ways to change them later, the wizard was set up with two pages. The first page collected the information that is essential to importing a project, this being the source of the files, a filter for file types and any non-standard search paths.

New Project from Existing Python Code Wizard page one

To avoid forcing the user to guess what each value means or how it behaves, lighter explanatory text is included directly in the window. (In earlier days these would have been tooltips, but with touch starting to become more prominent, requiring hovering is poor UI design.) The tone is deliberately casual and reassuring – one of the surprises people often find with Visual Studio is that adding an existing file will copy it into the project folder. When importing very large projects, it is far more desirable to leave the files alone and put the project in a nearby location. Because we support a ProjectHome element in our .pyproj files, we can treat any folder as the root of the project (this will no doubt be the subject of a post in the future).

The second page is entirely optional, and while it cannot be skipped entirely from the first, once users are familiar with the dialog it is very easy to click through without changing the default selections:

New Project from Existing Python Code Wizard page two

The two options on this page relate to Visual Studio settings, specifically, which version of Python should be used when running from within VS and which file should be used for F5/Ctrl+F5 execution (as opposed to using the “Start with/without Debugging” option on a specific file). Again, the light grey explanatory text reassures the user that any selection made here is not permanent and provides directions on how to make changes later. The second option – which file to run on F5 – also suggests that not all files will appear in the list. For performance reasons, only *.py (and *.pyw) files in the root directory are shown, since showing all files would require a recursive directory traversal (which is slow) and produce a much longer list of files (hard to navigate). Since Python does not allow the import statement to traverse up from the started script (in typical uses), most projects will have their main file in the root of the project. (That said, there is a chance that this aspect of the dialog will change for the next release, either by including all files, switching to a treeview or simply not being offered as an option.)

When “Finish” is clicked, the rest of the task is quite straightforward: the files in the provided path are scanned for all those matching the filter and the $variables$ in FromExistingCode.pyproj are replaced. This produces a .pyproj file that is then loaded normally. (Contrast with the other approach that creates an empty project and adds each file individually. This way is much faster.) Details are in the following section.

IWizard and replacementsDictionary

Template wizards in Visual Studio are implemented by providing the IWizard interface. The methods on this interface are called at various points to allow customisation of the template, but only one method is of interest here: RunStarted. The how-to page covers the process in detail, but the basic idea is that RunStarted displays the user interface and updates the set of replacement strings, which are then applied to existing template files.

The only template file used is FromExistingCode.pyproj, which contains five variables for replacement: $projecthome$, $startupfile$, $searchpaths$, $interpreter$ and $content$. While the first three are simple values, $interpreter$ will be replaced by the GUID and version (InterpreterId and InterpreterVersion properties) that represents the interpreter selected on page two of the wizard, and $content$ is replaced by the list of files and folders. Strictly, this is a slight misuse of the templating system, which is intended for values rather than code/XML generation, but it works and is quite efficient.

When RunStarted is called (by VS), a dictionary is provided for the wizard to fill in with these values. This means that a lot of processing takes place within this one function, which is generally not how callbacks like this should behave. However, in this case, it is perfectly appropriate to use modal dialogs and let exceptions propagate – in particular, WizardBackoutException should be thrown if the user cancels out of the dialog (unlike the WizardCancelledException, backing out returns the user to the template selection dialog).

RunStarted is implemented (along with the other methods) in ImportWizard.cs, with UI and XML generation separated into other methods to allow for easier testing.

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams) {
    try {
        var provider = new ServiceProvider((Microsoft.VisualStudio.OLE.Interop.IServiceProvider)automationObject);
        var settings = ImportWizardDialog.ShowImportDialog(provider);
 
        if (settings == null) {
            throw new WizardBackoutException();
        }
 
        SetReplacements(settings, replacementsDictionary);
    } catch (WizardBackoutException) {
        try {
            Directory.Delete(replacementsDictionary["$destinationdirectory$"]);
        } catch {
            // If it fails (doesn't exist/contains files/read-only), let the directory stay.
        }
        throw;
    } catch (Exception ex) {
        MessageBox.Show(string.Format("Error occurred running wizard:\n\n{0}", ex));
        throw new WizardCancelledException("Internal error", ex);
    }
}

One point worth expanding on is the Directory.Delete call in the cancellation handler. Because this is a new project wizard, VS creates the destination directory based on user input before the wizard starts. However, if the wizard is cancelled from within RunStarted, as opposed to failing before RunStarted can be called, the directory is not removed. To prevent the user from seeing empty directories appear in their Projects folder, we try and remove it. (That said, we don’t try very hard – if the directory has ended up with files in it already then it will not be removed.)

The .vstemplate File

The final piece of this feature is adding the entry to the New Project dialog and starting the wizard when selected by the user. Templates in Visual Studio are typically added by including .vstemplate files in a registered folder (optionally creating a registering a folder specific to an extension). These files are XML and specify the template properties and the list of files to copy to the destination directory. For templates that include wizards, an optional WizardExtension element can be added, as seen in FromExistingCode.vstemplate:

16
17
18
19
<WizardExtension>
  <Assembly>Microsoft.PythonTools.ImportWizard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</Assembly>
  <FullClassName>Microsoft.PythonTools.ImportWizard.Wizard</FullClassName>
</WizardExtension>

Importantly, the assembly name must be a fully qualified name, which is why we used a fixed version number for this file (the rest of our assemblies use an automatically generated build number, which is fine for compiled code but not so simple to include in a data file that is embedded in a .zip file). Wizard is the name of the class in ImportWizard.cs that implements IWizard, and so VS is able to instantiate Wizard and invoke RunStarted as part of creating the new project. The entire template is simply added to our existing templates directory, and VS discovers the .vstemplate file and includes it in the list.

One of the concerns with the previous version of this wizard was discoverability: it was not easy to find the feature. To completely solve this problem, we set the SortOrder value of the template to 10, which is very low and all but guarantees that it will always appear first in the list. So now the first option that will appear to both new and existing users of PTVS is a simple way to use their projects without having to add each file individually.

Summary

In this post we looked at the new New Project from Existing Code wizard that replaced the one from last time. Not using the existing implementation allowed us to design the wizard specifically for Python code and implement it efficiently. We also made the feature more discoverable by placing it first in the list of templates, which is how new projects are usually created. This feature was first released in Python Tools for Visual Studio 1.5RC.

New Project from Existing Code

In this post, we’ll be looking at how version one of a particular feature was implemented. The implementation was not very good (I can say that, since I contributed it), but it filled a necessary gap until it could be done better. Python Tools for Visual Studio 1.1 had this implementation, while PTVS 1.5 uses a completely different approach (that might be the subject of a future blog, but not this one).

When creating a new project in Visual Studio, you typically begin from a list of templates that are specific to your language:

Visual Basic project templates

If you already have a set of existing code, the obvious approach is to select “Empty Project” and add the files to that project. This situation is common when working on cross-platform projects, which often do not include Visual Studio projects, or when you want to migrate from earlier versions without introducing the compatibility ‘hacks’ provided by the upgrade wizard. An alternative to manually importing the files into an empty project is to use the “Project from Existing Code” wizard, which is available under the “New Project” submenu:

Project from Existing Code menu item

By default, this wizard only supports C#, VB and C++, but since very few Python projects ever come with project files, importing existing code is a very common task. This feature added basic support for Python projects to the existing wizard. The code is from changeset 156ceb1b1949, which was part of the original pull request that was merged into PTVS 1.0.

Adding Python to the list

The default wizard only provides support for C#, VB and C++.

Import Wizard window

Luckily, since Visual Studio is endlessly extensible, these project types are not hardcoded, but are found in the registry. For example, the C# entries are found in the Projects\{FAE04EC0-301F-11d3-BF4B-00C04F79EFBC}\ImportTemplates subkey of the Visual Studio key:

C# ImportTemplates registry entry

The meaning of each value is relatively straightforward, but to summarise:

WizardPageObjectAssembly
Full name of the assembly containing the wizard implementation.
WizardPageObjectClass
Full name of the call implementing the wizard.
ProjectType
The name (or a string resource within the project type’s package) to display in the dropdown box.
ImportProjectsDir
Directory containing the following three files.
ClassLibProjectFile
Filename of the project template for a class library project.
ConsoleAppProjectFile
Filename of the project template for a console application project.
WindowsAppProjectFile
Filename of the project template for a windowed application project.

Those last four items are specific to the Microsoft.VsWizards.ImportProjectFolderWizard.Managed.PageManager class, which is used for C# and VB projects only. The C++ wizard uses a different implementation that is very specific to C++, but the one for C# and VB is far more general and can be easily adapted to Python without requiring any new code (except to set up the registry entries). We could also provide a completely new class, but in the interest of minimising new code, that wasn’t done this time around.

To register the existing importer for Python projects, a ProvideImportTemplatesAttribute was created, since package attributes are how registry keys are added for PTVS (rather than specifying them in the installer). It was applied to the project package as:

[ProvideImportTemplates("#111",
                        PythonConstants.ProjectFactoryGuid,
                        "$PackageFolder$\\Templates\\Projects\\ImportProject",
                        "ImportWinApp.pyproj",
                        "ImportConApp.pyproj",
                        "ImportConApp.pyproj")]

The parameters map very closely to the values shown above for C#. PTVS has no concept of a library project, so we use a standard console project: the wizard implementation is… fragile… and while you could hope that leaving the class library template out would disable the option, in reality is simply causes an exception. Each template was created simply by using the existing template with the initial Program.py removed.

Adding this attribute is sufficient to make “Python Project” (the value of the “#111”) appear in the wizard, but not to actually import the project. This second step required a little more work inside the implementation of the PTVS project system.

The AddFromDirectory method

Among all the classes required to implement the project system, it is very rare to see dead code. Methods are only implemented if they are called, and as it turned out, a method required for importing existing code had never been written. This was the AddFromDirectory method in OAProjectItems.cs.

In brief (not that I can be much more brief than that MSDN page), AddFromDirectory recursively adds a folder and all the files and folders it contains into the project. The directory provided is returned as a new project folder (as if the user had clicked on “New Folder” themselves), which means that this function cannot actually be used to import the entire project. As a workaround, the wizard implementation adds any top-level files directly, then calls AddFromDirectory for each top-level folder.

Implementing AddFromDirectory is very simple, since the existing AddDirectory and AddFromFile methods can be used. The original implementation filtered files to only include those that could be imported from Python (in effect, *.py and *.pyw files in directories containing an __init__.py file), though this was later relaxed based on user feedback.

Problems

While this approach is very simple, it has a number of drawbacks. Those that have already been mentioned include an irrelevant option for a “Class Library” and the inability to filter top-level files and directories. These could be easily resolved by substituting another wizard class, but this is actually the point where the extensibility breaks down.

Import wizards are .NET classes that implement Microsoft.VsWizards.ImportProjectFolderWizard.IPageManager from the Microsoft.VisualStudio.ImportProjectFolderWizard.dll assembly that ships with Visual Studio. Unfortunately, this assembly is not stored anywhere that can be safely referenced without assuming the full VS installation path, cannot be redistributed (and probably cannot be loaded multiple times from different locations anyway), and is not guaranteed to be loaded before another assembly containing wizards. Basically, while it is possible to create a new wizard, it is not possible to safely load that wizard, making this idea pretty useless.

The final problem that was encountered was discoverability. Despite documentation, a video and announcements, it is not easy to find this feature, and even the other languages suffer from this. It would seem that people generally use other ways to open the New Project window (since there are at least four) and so never even see the “From Existing Code” option. In the 1.5 release of PTVS we’ve tried to solve this by providing the wizard as an item in the normal templates list:

Python project templates with PTVS 1.5

Summary

It is possible to extend the “New Project from Existing Code” wizard by providing some template files and registry entries. The project system that the wizard is for has to implement AddFromDirectory on project items, since this is the function that performs most of the work. Unfortunately, extending by providing a new wizard implementation is not possible because of how the dependent assemblies are distributed and loaded. This feature was originally in Python Tools for Visual Studio version 1.0, and was replaced in version 1.5 with a more appropriate and discoverable wizard.