Chapter 8. Managing Solutions and Projects Programmatically

Microsoft® Visual Studio® 2005 is rich with tools to help you manage and complete your programming tasks. One of these tools is the project management system. Projects are where files are created, managed, and compiled to create the resulting program. In this chapter, you'll discover how you can manipulate solutions and projects by using the automation object model.

Working with Solutions

In Visual Studio, a solution is the basic unit of project management. A solution is a container for any number of projects that work together to create the whole of a program. Each project within a solution can contain code files that are compiled to create the program, folders to make managing the files easier, and references to other software components that a project might use. You manage a solution through the Solution Explorer tool window, where you can add, remove, and modify projects and the files they contain. When a solution file is opened, a node is created within Solution Explorer that represents the solution, and each project added to this solution appears as a subnode of the top-level node.

Within the Visual Studio object model, a solution is represented by the EnvDTE.Solution object, which you can retrieve using the Solution property of the DTE object, as shown in the following macro:

Sub GetSolution()
    Dim solution As EnvDTE.Solution
    solution = DTE.Solution
End Sub

Creating, Loading, and Unloading Solutions

To use the Solution object and its methods and properties, you don't need to create or open a solution file from disk. You can use the Solution object even though the solution node in Solution Explorer might not be visible. Visual Studio always has a solution open, even if it exists only in memory and not on disk. If you open a solution file from disk and the in-memory solution is not dirty (modified but not saved to disk), this in-memory solution is discarded and the solution on disk is loaded. If the in-memory solution has been modified (such as by having a new or existing project added), when you close it, you'll be prompted to save the solution to disk.

To save a solution programmatically, you can use the method Solution.SaveAs; you pass it the full path, including the file name and the .sln file extension, to where the solution should be stored on disk. However, using the Solution.SaveAs method might not always work and can generate an exception because you must first save a solution file to disk or load it from an existing solution file on disk before you can use the SaveAs method. To allow saving of the solution file, you can use the Create method. You use this method to specify information such as where the solution file should be saved and the name of the solution. By combining the Create and SaveAs methods, you can create and save the solution:

Sub CreateAndSaveSolution()
    DTE.Solution.Create("C:", "Solution")
    DTE.Solution.SaveAs("C:Solution.sln")
End Sub

Once you create a solution file and save it to disk, whether through the user interface or the object model, you can use the Solution.Open method to open it. Using the file path given in the CreateAndSaveSolution macro, we can open our solution as shown here:

DTE.Solution.Open("C:Solution.sln")

When you call this method, the currently open solution is discarded, and the specified solution file is opened. When an open solution is closed to make way for the solution file that is being loaded, the user won't be notified that the current solution is being closed, even if the current solution has been modified. This means that you won't be given the option to save any changes. A macro or an add-in can use the ItemOperations.PromptToSave property to offer the option of saving a solution. The ItemOperations object, which is accessed from the DTE.ItemOperations property, contains various file manipulation methods and properties. One property of this object, PromptToSave, displays a dialog box that gives you the option to save modified files and returns a value indicating which button was clicked. This property also saves the files for you if the appropriate user interface button is selected. This property won't show the dialog box if no files need to be saved—it will immediately return a value indicating that you clicked the OK button. You can combine the PromptToSave property with the Open method to properly save modified files and open a solution:

Sub OpenSolution()
    Dim promptResult As vsPromptResult
    'Offer to save any open and modified files:
    promptResult = DTE.ItemOperations.PromptToSave
    'If the user pressed anything but the Cancel button,
    ' then open a solution file from disk:
    If promptResult <> vsPromptResult.vsPromptResultCancelled Then
        DTE.Solution.Open("C:Solution.sln")
    End If
End Sub

You've learned how to create, save, and open a solution—the only piece of the life cycle of a solution you haven't learned is how to close it. The Solution object supports the method Close, which you can use to close a solution file. This method accepts one optional Boolean parameter, which you can use to direct Visual Studio to save the file when you close it. If you pass the value true for this parameter, the solution file is saved before you close it; if you set it to false, any changes to the file are discarded.

Enumerating Projects

The Solution object is a collection of Project objects, and because it is a collection, it has an Item method that you can use to find a project within the solution. This method supports the numeric indexing method, as the Item method of other collection objects do, but it also supports passing a string to find a project. The string form of the Solution.Item method is different from that of other Item methods, however. Rather than taking the name of a project, Solution.Item requires the unique name of a project. A unique name, as its name indicates, uniquely identifies a project among all other projects within a solution. Unique names are used to index the projects collection because Visual Studio might eventually support loading two projects that have the same name but are located in different folders on disk. (Visual Studio requires that all projects within a solution have a name that is different from all other projects.) Because loading two or more projects with the same name might be allowed in a future version of Visual Studio, the name alone isn't enough to differentiate one project from another when you call the Item method. You can retrieve the unique name of a project by using the Project.UniqueName property. The following macro retrieves this value for all the projects loaded into a solution:

Sub EnumProjects()
    Dim project As EnvDTE.Project
    For Each project In DTE.Solution
        MsgBox(project.UniqueName)
    Next
End Sub

The Solution object isn't the only collection of all projects that are loaded. The Solution object has a Projects property, which also returns a collection of the available projects and works in the same way that the Solution object does for enumerating and indexing projects. It might seem redundant to have this same functionality in two places, but the Visual Studio object model team, after performing usability studies, found that developers didn't recognize the Solution object as a collection. The team, therefore, added this Projects collection to help developers find the list of projects more easily. You can rewrite the EnumProjects macro, as follows, so it can use the Projects collection:

Sub EnumProjects2()
    Dim project As EnvDTE.Project
    For Each project In DTE.Solution.Projects
        MsgBox(project.UniqueName)
    Next
End Sub

You can find the list of projects by using the Solution and Projects collections, but at times you'll need to find the projects that you've selected within the Solution Explorer tree view window. The DTE.ActiveSolutionProjects property, when called, looks at the items selected within Solution Explorer. If a project node is selected, the Project object for that selected project is added to a list of objects that will be returned. If a project item is selected, the project containing that item is also added to the list returned. Finally, any duplicates are removed from the list, and the list is returned. The following macro demonstrates the use of this property:

Sub FindSelectedProjects()
    Dim selectedProjects As Object()
    Dim project As EnvDTE.Project
    selectedProjects = DTE.ActiveSolutionProjects
    For Each project In selectedProjects
        MsgBox(project.UniqueName)
    Next
End Sub

Adding Projects to a Solution

While you can use the object model to enumerate projects within a solution, there may be times when you need to add a new or existing project to the solution. You add projects to the solution by using the AddFromTemplate and AddFromFile methods on the Solution object. AddFromFile takes a path to an existing project on disk and inserts that project into the solution. AddFromTemplate will create a new project within the solution based upon a VSTemplate file—the same templates that we showed you how to create in Chapter 4. Calling this method will invoke the template wizard, causing the project to be copied into a destination folder that you specify and causing all replacement tokens within the files for that project to be replaced with the appropriate values. The signature for AddFromTemplate is

public EnvDTE.Project AddFromTemplate(string FileName,
        string Destination, string ProjectName,
        bool Exclusive = false)

Here are the arguments:

  • FileName The full path to the project template.

  • Destination The location on disk to which the project and the files it references are copied. The wizard should create this destination path before AddFromTemplate is called.

  • ProjectName The name assigned to the project file and the name in Solution Explorer where it has been copied. Don't attach the extension of the project type to this argument.

  • Exclusive If this parameter is set to true, the current solution is closed and a new one is created before the template project is added. If this parameter is false, the solution isn't closed and the newly created project is added to the currently open solution.

Note

If the Exclusive parameter is set to true when AddFromFile or AddFromTemplate is called, the existing solution is closed without the user being given the option to save any modified files. You should give the user the option to save by calling the ItemOperations.PromptToSave property before calling AddFromTemplate or AddFromFile.

You can find the path to a template that is installed for all users with the method Solution.GetProjectTemplate. To find the path to the template, you need to supply the template name that you want to find, as well as the programming language of the template that you want to add. The template name is the .zip file that contains all the files necessary to re-create a project. For example, the Microsoft Visual C#® console application template is named ConsoleApplication.zip, and the language name for Visual C# is CSharp, so a macro that finds this template would look like so:

Sub ProjectTemplatePath()
    Dim solution2 As EnvDTE80.Solution2
    Dim CSConsoleTemplatePath As String

    solution2 = CType(DTE.Solution, EnvDTE80.Solution2)
    CSConsoleTemplatePath = solution2.GetProjectTemplate(_
           ConsoleApplication.zip", "CSharp")
    MsgBox(CSConsoleTemplatePath)
End Sub

You can create a project based upon a template stored in the My Documents folder, but because there is no automated way of calculating the path to this template, you need to calculate it yourself. When a .zip file is placed into the appropriate folder under the My DocumentsVisual Studio 2005ProjectTemplates folder, and Visual Studio notices the new template (because the New Project dialog box has been shown), it will extract that template into a cache folder at C:Documents and SettingsusernameApplication DataMicrosoft VisualStudio8.0ProjectTemplatesCache. We can use the .NET Framework method Environment.GetFolderPath to find the first portion of this path, but we will need to construct the rest and add to it the language and template name ourselves to find the full path of the .vstemplate file. If you have a Microsoft Visual Basic® project template named MyTemplate.zip in the correct place, you can find the path to the .vstemplate file with code such as this:

Sub UserProjectTemplatePath()
    Dim projectTemplatePath As String
    projectTemplatePath = System.Environment.GetFolderPath( _
            System.Environment.SpecialFolder.ApplicationData)
    projectTemplatePath = System.IO.Path.Combine(projectTemplatesPath, _
            "MicrosoftVisualStudio8.0ProjectTemplatesCache")
   'projectTemplatesPath contains the path for all templates, now add to
   '  the path using information specific to the template to find:
   projectTemplatePath = System.IO.Path.Combine(projectTemplatesPath, _
           "Visual BasicMyTemplate.zipMyTemplate.vstemplate")
End Sub

Capturing Solution Events

As you interact with a solution, Visual Studio fires events that allow an add-in or a macro to receive notifications about which actions you perform. These events are fired through the SolutionEvents object, which you can access through the Events.SolutionEvents property. You can capture solution events in the usual way—by opening the EnvironmentEvents module of any macro project, selecting the SolutionEvents object in the left drop-down list at the top of the code editor window, and selecting the event name in the right drop-down list of this window.

Here are the signatures and meanings for the events available for a solution:

  • void Opened() This event is fired just after a solution file has been opened.

  • void Renamed(string OldName) This event handler is called just after a solution file has been renamed on disk. The only argument passed to this handler is the full path of the solution file just before it was renamed.

  • void ProjectAdded(EnvDTE.Project Project) This event is fired when a project is inserted into the solution. One argument is passed to this event handler—the EnvDTE.Project object for the project that was inserted.

  • void ProjectRenamed(EnvDTE.Project Project, string OldName) This event is fired when a project within the solution has been renamed. The event handler is passed two arguments. The first is of type EnvDTE.Project and is the object for the project that has just been renamed. The second parameter is a string that contains the full path of the project file before it was renamed.

  • void ProjectRemoved(EnvDTE.Project Project) This event is fired just before a project is removed from the solution. This event handler receives as an argument the EnvDTE.Project object for the project that is being removed. Just as when you use the BeforeClosing event, you shouldn't modify the project being removed within this event because the project has already been saved to disk (if you specified that the file be saved) before being removed, and any modifications to the project will be discarded.

  • void QueryCloseSolution(ref bool fCancel) This event is fired just before Visual Studio begins to close a solution file. The handler for this event is passed one argument—a reference to a Boolean variable. An add-in or a macro can block a solution from being closed by setting this parameter to true, or it can allow the solution to be closed by setting the parameter to false. You should take care when you choose to stop the solution from being closed—users might be unpleasantly surprised if they try to close the solution but a macro or an add-in disallows it.

  • void BeforeClosing() This event is fired just before the solution file is about to close but after it has been saved (if you specified the option to save). Because this event is fired after the chance to save the file has passed, the event handler shouldn't make any changes to the solution because those changes will be discarded.

  • void AfterClosing() This event is fired just after the solution file has finished closing.

The sample named SolutionEvents, which is among the book's sample files, demonstrates connecting to each of these events. Once you load this sample, as each event is fired, the add-in displays a message box showing a bit of information about the event that was fired. The QueryCloseSolution event handler also offers the option of canceling the closing of the solution. The source code for this add-in sample is shown in Example 8-1.

Example 8-1. SolutionEvents.cs, the source code for the solution events add-in

namespace SolutionEvents
{
    using System;
    using Microsoft.VisualStudio.CommandBars;
    using Extensibility;
    using EnvDTE;
    using EnvDTE80;
    using System.Windows.Forms;

    public class Connect : Object, IDTExtensibility2
    {
        public Connect()
        {
        }

        public void OnConnection(object application, ext_ConnectMode connectMode, _
                object addInInst, ref Array custom)
        {
            applicationObject = (DTE2)application;
            addInInstance = (AddIn)addInInst;
            //Set the solutionEvents delegate variable using the
            // DTE.Events.SolutionEvents property:
   solutionEvents =
      (EnvDTE.SolutionEvents)applicationObject.
Events.SolutionEvents;

            //Setup all available event handlers by creating a new
            // instance of the appropriate delegates:
            solutionEvents.AfterClosing += new
               _dispSolutionEvents_AfterClosingEventHandler(AfterClosing);
            solutionEvents.BeforeClosing += new
               _dispSolutionEvents_BeforeClosingEventHandler
(BeforeClosing);
            solutionEvents.Opened += new
_dispSolutionEvents_OpenedEventHandler(Opened);
            solutionEvents.ProjectAdded += new
_dispSolutionEvents_ProjectAddedEventHandler(ProjectAdded);
            solutionEvents.ProjectRemoved += new
_dispSolutionEvents_ProjectRemovedEventHandler
(ProjectRemoved);
            solutionEvents.ProjectRenamed += new
_dispSolutionEvents_ProjectRenamedEventHandler
(ProjectRenamed);
            solutionEvents.QueryCloseSolution += new
               _dispSolutionEvents_QueryCloseSolutionEventHandler
(QueryCloseSolution);
            solutionEvents.Renamed += new
               _dispSolutionEvents_RenamedEventHandler(Renamed);
        }

        public void OnDisconnection(ext_DisconnectMode disconnectMode,
 ref Array custom)
        {
            //The Add-in is closing. Disconnect the event handlers:
            solutionEvents.AfterClosing -= new
_dispSolutionEvents_AfterClosingEventHandler
(AfterClosing);
            solutionEvents.BeforeClosing -= new
_dispSolutionEvents_BeforeClosingEventHandler
(BeforeClosing);
            solutionEvents.Opened -= new
_dispSolutionEvents_OpenedEventHandler(Opened);
            solutionEvents.ProjectAdded -= new
 _dispSolutionEvents_ProjectAddedEventHandler(ProjectAdded);
            solutionEvents.ProjectRemoved -= new
               _dispSolutionEvents_ProjectRemovedEventHandler
(ProjectRemoved);
            solutionEvents.ProjectRenamed -= new
_dispSolutionEvents_ProjectRenamedEventHandler
(ProjectRenamed);
            solutionEvents.QueryCloseSolution -= new
_dispSolutionEvents_QueryCloseSolutionEventHandler
(QueryCloseSolution);
            solutionEvents.Renamed -= new
_dispSolutionEvents_RenamedEventHandler(Renamed);
        }

        public void OnAddInsUpdate(ref Array custom)
        {
        }

        public void OnStartupComplete(ref Array custom)
        {
        }

        public void OnBeginShutdown(ref Array custom)
        {
        }

        //SolutionEvents.AfterClosing delegate handler:
        public void AfterClosing()
        {
             MessageBox.Show("SolutionEvents.AfterClosing", "Solution Events");
        }

        //SolutionEvents.BeforeClosing delegate handler:
        public void BeforeClosing()
        {
            MessageBox.Show("SolutionEvents.BeforeClosing", "Solution Events");
        }

        //SolutionEvents.Opened delegate handler:
        public void Opened()
        {
            MessageBox.Show("SolutionEvents.Opened", "Solution Events");
        }

        //SolutionEvents.ProjectAdded delegate handler.
        //Display the UniqueName of the project that has been added.
        public void ProjectAdded(EnvDTE.Project project)
        {
            MessageBox.Show("SolutionEvents.ProjectAdded
Project: " +
 project.UniqueName, "Solution Events");
        }

        //SolutionEvents.ProjectRemoved delegate handler.
        //Display the UniqueName of the project that has been added.
        public void ProjectRemoved(EnvDTE.Project project)
        {
            MessageBox.Show("SolutionEvents.ProjectRemoved
Project: " +
 project.UniqueName, "Solution Events");
        }

        //SolutionEvents.ProjectRemoved delegate handler.
        //Display the UniqueName of the project that has been renamed,
        // and the full path file before it was renamed.
        public void ProjectRenamed(EnvDTE.Project project, string oldName)
        {
            MessageBox.Show("SolutionEvents.ProjectRenamed
Project: " +
 project.UniqueName + "
Old project name: " + oldName,
 "Solution Events");
        }

        //SolutionEvents.QueryCloseSolution delegate handler.
        //Asks if closing the solution should be canceled.
        public void QueryCloseSolution(ref bool cancel)
        {
            if (MessageBox.Show(
"SolutionEvents.QueryCloseSolution
Continue with close?",
               "Solution Events", MessageBoxButtons.YesNo) ==
               DialogResult.Yes)
                cancel = false;
            else
                cancel = true;
        }

        //SolutionEvents.QueryCloseSolution delegate handler.
        //Displays the full path the solution before and after it was renamed.
        public void Renamed(string oldName)
        {
            MessageBox.Show(
"SolutionEvents.Renamed
New solution name: " +
applicationObject.Solution.FullName +
"
Old solution name: " + oldName, "Solution Events");
        }

        private DTE2 applicationObject;
        private AddIn addInInstance;

        //The delegate handler variable:
        private EnvDTE.SolutionEvents solutionEvents;
    }
}

Working with Project Items

Solutions manage a number of projects, and each project manages the files that are built into a program. Each project contains files that can be enumerated and programmed.

Enumerating Project Items

Files within a project are arranged hierarchically. A project can contain any number of files and one or more folders, which themselves can contain additional files and folders. To match this project hierarchy, the project object model is also arranged hierarchically, with the ProjectItems collection representing the nodes that contain items and the ProjectItem object representing each item within this collection. To enumerate this hierarchy, you use the ProjectItems and ProjectItem objects. The following macro walks the first level of the hierarchy of the ProjectItems and ProjectItem objects by using the Project.ProjectItems property to obtain the top-level ProjectItems object:

Sub EnumTopLevelProjectItems()
    Dim projItem As EnvDTE.ProjectItem
    Dim projectProjectItems As EnvDTE.ProjectItems
    Dim project As EnvDTE.Project

    'Find the first project in a solution:
    project = DTE.Solution.Projects.Item(1)
    'Retrieve the collection of project items:
    projectProjectItems = project.ProjectItems
    'Walk the list of items in the collection:
    For Each projItem In projectProjectItems
        MsgBox(projItem.Name)
    Next
End Sub

Some items within a project, such as a folder, are both an item within the project hierarchy and a container of other files and folders. Because these folders are both items and collections of items, a folder is represented in the project model hierarchy with both a ProjectItem object and a ProjectItems object. You can determine whether a ProjectItem node is also a container of more ProjectItem nodes by calling the ProjectItem.ProjectItems property, which returns a ProjectItems collection if the node can contain subitems. You can enumerate all the ProjectItem and ProjectItems objects within a project by writing a recursive macro function such as this:

Sub EnumProjectItems(ByVal projItems As EnvDTE.ProjectItems)
    Dim projItem As EnvDTE.ProjectItem
    'Find all the ProjectItem objects in the given collection:
    For Each projItem In projItems
        MsgBox(projItem.Name)
        'And walk any items the current item may contain:
        EnumProjectItems(projItem.ProjectItems)
    Next
End Sub

Sub EnumProject()
    Dim project As EnvDTE.Project
    'Find the first project in a solution:
    project = DTE.Solution.Projects.Item(1)
    EnumProjectItems(project.ProjectItems)
End Sub

The EnumProject macro first finds the ProjectItems collection of a given project, and then it calls the EnumProjectItems subroutine, which will find all the ProjectItem objects that the collection contains. If the ProjectItem object is itself a collection, it will recursively call the EnumProjectItems subroutine to display the items it contains.

Folders aren't the only items that can contain a collection of ProjectItem objects. Files such as Windows Forms and Web Forms are also collections of files. Each of these file types has associated resource files (in the form of .resx files), and they can also have an associated code-behind file. In the default state, Solution Explorer won't give any indication of whether these files are containers for other files, but you can modify it to show the files that these file types contain. Choose Show All Files from the Project menu to show all form files as expandable in the tree view that makes up Solution Explorer. When the EnumProject macro (shown earlier) is run, the ProjectItem.ProjectItems property returns a collection that contains the ProjectItem objects for these subitems. Code such as the EnumProject macro will return the same values whether or not the Show All Files menu command has been chosen. This command affects only the Solution Explorer user interface.

You can combine the techniques for enumerating files and files within folders to find a specific item within a project. Suppose you've created a Windows Forms application solution and modified the project to look like that shown in Figure 8-1.

A Windows Forms application with nested resources

Figure 8-1. A Windows Forms application with nested resources

Using the ProjectItem object and ProjectItems collection, you can write a macro such as the following to locate the Bitmap1.bmp file:

Sub FindBitmap()
    Dim project As EnvDTE.Project
    Dim projectProjectItems As EnvDTE.ProjectItems
    Dim resourcesProjectItem As EnvDTE.ProjectItem
    Dim resourcesProjectItems As EnvDTE.ProjectItems
    Dim bitmapsProjectItem As EnvDTE.ProjectItem
    Dim bitmapsProjectItems As EnvDTE.ProjectItems
    Dim bitmapProjectItem As EnvDTE.ProjectItem

    'Get the project:
    project = DTE.Solution.Item(1)
    'Get the list of items in the project:
    projectProjectItems = project.ProjectItems
    'Get the item for the Resources folder:
    resourcesProjectItem = projectProjectItems.Item("Resources")
    'Get the collection of items in the Resources folder:
    resourcesProjectItems = resourcesProjectItem.ProjectItems
    'Get the item for the Bitmaps folder:
    bitmapsProjectItem = resourcesProjectItems.Item("Bitmaps")
    'Get the collection of items in the Bitmaps folder:
    bitmapsProjectItems = bitmapsProjectItem.ProjectItems
    'Get the item for the Bitmap1.bmp file:
    bitmapProjectItem = bitmapsProjectItems.Item("Bitmap1.bmp")
    MsgBox(bitmapProjectItem.Name)
End Sub

You can walk down the tree of the ProjectItem and ProjectItems hierarchy to find a specific file, but sometimes you might need a quicker and easier way of locating the ProjectItem object for a file with a specific file name in a project. You can use the FindProjectItem method of the Solution object to find an item by passing a portion of the file path to where the file is located on disk. For example, suppose two add-in projects have been created (using the Add-in Wizard) in a folder you created called Addins located in the root of drive C. Each of these two add-ins, MyAddin1 and MyAddin2, contains a file named Connect.cs. You could use the following macro to locate the Connect.cs file in either project:

Sub FindItem()
    Dim projectItem As EnvDTE.ProjectItem
    projectItem = DTE.Solution.FindProjectItem("Connect.cs")
End Sub

However, because FindProjectItem returns any file that matches this file name, you can't tell which ProjectItem will be returned—the ProjectItem object for the Connect.cs in MyAddin1 or the ProjectItem object for Connect.cs in MyAddin2. To refine the search, you can supply a bit more of the file path as the specified file name, as shown in the following macro, which adds the name of the folder on disk that contains the MyAddin1 version of Connect.cs:

Sub FindItemWithFolder()
    Dim projectItem As EnvDTE.ProjectItem
    projectItem = DTE.Solution.FindProjectItem("MyAddin1Connect.cs")
End Sub

Of course, just as you can specify a portion of the path to find the ProjectItem, you can use the whole path to zero in on the exact item you want:

Sub FindItemWithFullPath()
    Dim projectItem As EnvDTE.ProjectItem
    projectItem = _
        DTE.Solution.FindProjectItem("C:AddinsMyAddin1Connect.cs")
End Sub

Adding and Removing Project Items

You can add new files to a project in two ways. The first way is to use the AddFromDirectory, AddFromFile, AddFromFileCopy, and AddFromTemplate methods of the ProjectItems interface (which we'll discuss shortly). The second way is to use the ItemOperations object. This object offers a number of file manipulation methods to help make working with files easier. The difference between using the methods of the ProjectItems object and the methods of ItemOperations is that the ProjectItems object gives an add-in or a macro more fine-grained control over where, within a project, the new file is created. The ItemOperations object methods are more user-interface oriented; they add the new file to the project or folder that is selected in Solution Explorer, or, if a file is selected, they add the item to the project or the folder containing that file. These features help make macro recording possible. If you start the macro recorder and then add a file into a project, a call to one of the methods of the ItemOperations object is recorded into the macro. The selected item is where files are added when the proper method is called.

One method of the ItemOperations object, AddExistingItem, takes as its only argument the path to a file on disk and adds this file to the selected project or folder within a project. Depending on the type of project, the file might be copied to the project folder before being added, or a reference might be added to the file without copying the file. Visual Basic and Visual C# projects are folder-based, which means that the project hierarchy shown in Solution Explorer is mirrored on disk, and any files within the project must be in the folder containing the project or in one of its subfolders. Microsoft Visual C++® projects work a little differently: a file that is part of the project can be located anywhere on disk, and it doesn't need to be within the folder containing the project or a child folder of that folder. For instance, suppose a file named file.txt is located in the root directory of the C drive. If we run the macro

Sub AddExistingItem()
    DTE.ItemOperations.AddExistingItem("C:file.txt")
End Sub

and the item selected in Solution Explorer is a Visual C# or a Visual Basic project or one of its children, file.txt will be copied into the folder or subfolder containing the project file and then added to the project. But if the selected item is a Visual C++ project, the file will be left in place and a reference will be added to this file.

Whereas AddExistingItem inserts a file from disk into a project, AddNewItem creates a new file and adds it to the project. This method takes two arguments and has the following method signature:

public EnvDTE.ProjectItem AddNewItem(string Item = "GeneralText File",
        string Name = "")

You can add a new item through the user interface by right-clicking on a project and choosing Add | Add New Item from the shortcut menu, which brings up the Add New Item dialog box.

The Add New Item dialog box is related to the AddNewItem method in that the first parameter of AddNewItem is the type of file to be added. You can find this file type by using the Add New Item dialog box. Figure 8-2 shows two different Add New Item dialog boxes—the Add New Item dialog box for a Visual C# project (you will also see a similar dialog box for Visual Basic and Microsoft Visual J#® projects) and the dialog box you use when trying to add a new item to a Visual C++ project. If the dialog box you see is of the first type, the name is computed by first taking the language name, such as Visual C#, Visual Basic, or Visual J#, adding the string "Project Items", and appending the name of the file selected within the list view on the right side of the dialog box. This means that the string to add a class file to a Visual C# project would be "Visual C# Project ItemsClass". If the dialog that appears looks like that in the second Add New Item dialog box in Figure 8-2, the file type is calculated by taking the path to the item selected in the tree view, with each portion of the path separated by a backslash, and then adding the title of the item in the list on the right side of the dialog box. So, for example, when a Windows Forms file is added to the project, the topmost node of the tree view (Visual C++) is concatenated with the backslash character. Next, the string UI is appended to this string (because it is the tree node that contains the Windows Forms item to be added), followed by another backslash. Finally, the name of the item shown in the right panel of the dialog box, the string Windows Form, is added, resulting in the string that can be passed to AddNewItem: Visual C++UIWindows Form. The second argument of this method is simply the name of the file to create, with the file name extension appended. If the file name parameter passed is an empty string, a default file name is generated and used.

The Add New Item dialog box for a Visual C# and a Visual C++ project

Figure 8-2. The Add New Item dialog box for a Visual C# and a Visual C++ project

The ItemOperations object will add new and existing files to the selected project within Solution Explorer, but you will not always want to modify the selection to add an item to a specific solution. The ProjectItems collection has a series of methods for adding new and existing files to any project in a solution: AddFromDirectory, AddFromFileCopy, AddFromFile, and AddFromTemplate. AddFromDirectory accepts as a parameter the path to a folder on disk; this folder is searched recursively, causing all its contained files and subfolders to be added to the project. AddFromFileCopy and AddFromFile both perform the same basic operation, adding a reference to the specified file on disk to the project. However, AddFromFileCopy copies the file into the project's directory structure before adding this file to the project while AddFromFile adds a link to the item in whichever folder the file is contained. AddFromFileCopy differs from the AddFromTemplate method of the ProjectItems collection (not to be confused with the AddFromTemplate method of the Solution object) in that AddFromTemplate copies the file into the folder on disk for the project and then the project might make some modifications to the file after the files are added; if the template file has the .vstemplate extension, the template wizard is started and called to add the file to the project.

Here are the signatures, the parameters, and the meanings of the parameters for these methods:

EnvDTE.ProjectItem AddFromDirectory(string Directory)
EnvDTE.ProjectItem AddFromFileCopy(string FilePath)
EnvDTE.ProjectItem AddFromFile(string FileName)
EnvDTE.ProjectItem AddFromTemplate(string FileName, string Name)
  • Directory The source folder on disk. Searches for files and subfolders begin with this folder.

  • FilePath/FileName The location of the file to copy or add a reference to.

  • Name The resulting name of the file. This name should have the extension of the file type.

Each of these methods returns a ProjectItem, an object that can be used to perform operations on the file that was added (such as opening the file or accessing the file's contents).

Note

All of the Add methods on both the Solution and ProjectItems objects will return null or Nothing (depending upon the programming language you are using) if you pass a .vstemplate file as a template name. This is because a wizard could add 0, 1, or many items to a solution or project. If a wizard does not add any items, null or Nothing is a logical value to return. If multiple items are added, because the Add methods can return only one Project or ProjectItem object, null or Nothing is the correct value to return because the wizard would need to select one item arbitrarily to return. If the template wizard were to add one item, it would be confusing if a Project or ProjectItem object were returned, so, in this case, null or Nothing is also returned. Also, some of the Add methods will allow you to specify a target file name, but the wizard does not necessarily honor that target file name, so finding the item that you suggested a name for may not work. There is another reason for this value to be returned if you pass a .vstemplate as a template file: wizards do not have a mechanism for returning an item to Visual Studio indicating which item has been added, and without the ability to pass back an object, there is nothing to return other than null or Nothing.

Finding the path to an installed template for adding an item to a project is similar to finding a template for adding a project. For templates installed for all users, rather than using the GetTemplatePath method as you do for projects, you use the GetProjectItemTemplate for project items. This code uses the GetProjectItemTemplate method to find the path to the .vstemplate file for a Visual Basic module:

Sub ProjectItemTemplatePath()
    Dim solution2 As EnvDTE80.Solution2
    Dim VBModuleTemplatePath As String
    solution2 = CType(DTE.Solution, EnvDTE80.Solution2)
    VBModuleTemplatePath = _
           solution2.GetProjectItemTemplate("Module.zip",_
           "VisualBasic")
    MsgBox(VBModuleTemplatePath)
End Sub

And just as for project templates installed by the user, you will need to use a brute-force strategy to find the path to the user-installed project item template. Project item templates are extracted into a folder named ItemTemplatesCache when the appropriate Add New Item dialog box is shown. This macro code will file a new project item template, named MyTemplate, for the Visual Basic language:

Sub UserProjectItemTemplatePath()
    Dim projectItemTemplatePath As String
    projectItemTemplatePath = System.Environment.GetFolderPath _
           (System.Environment.SpecialFolder.ApplicationData)
    projectItemTemplatePath = System.IO.Path.Combine _
           (projectItemTemplatePath, _
           "MicrosoftVisualStudio8.0ItemTemplatesCache")
    'projectItemTemplatePath contains the path for all templates,
    'now add to the path
    '  using information specific to the template to find:
    projectItemTemplatePath = System.IO.Path.Combine _
           (projectItemTemplatePath, _
           "Visual BasicMyTemplate.zipMyTemplate.vstemplate")
End Sub

You might occasionally need to remove an item that has been added to a project because you no longer need it. The ProjectItem object supports two methods for removing items from the project: Remove and Delete. These two methods both remove an item from the project, but Delete is more destructive because it also erases the file from disk by moving it into the computer's Recycle Bin.

Working with Language-Specific Project Objects

The Visual Studio project object model was designed to provide functionality common to all project types. However, some projects can support additional, unique functionality. For example, a Visual C# project has a references node within its project, but a Setup project does not. If you could programmatically add and remove these references, you'd get a lot of flexibility in writing add-ins, macros, and wizards. To enable such project-specific programming, the Visual Studio project object model is extensible, allowing each project type to offer additional methods and properties beyond those defined by the Project object.

You can access the specific object type by using the Object property of the Project object. This property returns an object of type System.Object, which you can convert to the object model type supported by a specific language. The most commonly used project extension is the VSProject object, which is available for the Visual Basic, Visual J#, or Visual C# project types.

VSProject Projects

VSLangProj.VSProject is the interface that defines extensions to the EnvDTE.Project object for Visual J#, Visual Basic, or Visual C# projects. Once you've retrieved the EnvDTE.Project interface for one of these project types, you can get to the VSLangProj.VSProject interface by calling the Project.Object property. The following macro code, which assumes that the first project in the Projects collection is a Visual Basic, Visual C#, or Visual J# project, retrieves the VSProject object for that project:

Sub GetVSProject()
    Dim project As EnvDTE.Project
    Dim vsproject As VSLangProj.VSProject
    project = DTE.Solution.Projects.Item(1)
    vsproject = CType(project.Object, VSLangProj.VSProject)
End Sub

References

References are pointers to software components that a project can use to reduce the amount of code a programmer needs to write. A project uses the type of information contained within a reference to display information in the form of IntelliSense® statement completion. A reference also provides information to the compiler for resolving symbols used in programming code. A reference can be an assembly or another project loaded into the solution, and you can create references to COM components by wrapping the COM-object type information library with an interop assembly. You can add references through the user interface by right-clicking on the References node in a Visual Basic or Visual C# project, choosing Add Reference from the shortcut menu, and then selecting a component in the dialog box that appears.

Using the VSLangProj.References object, you can enumerate, add, or remove references. To get to the References object, use the VSProject.References property. For example, the following code retrieves the References object and then enumerates the references that have been added to a project:

Sub EnumReferences()
    Dim proj As EnvDTE.Project
    Dim vsproj As VSLangProj.VSProject
    Dim references As VSLangProj.References
    Dim reference As VSLangProj.Reference
    proj = DTE.Solution.Projects.Item(1)
    vsproj = proj.Object
    references = vsproj.References
    For Each reference In references
        MsgBox(reference.Name)
    Next
End Sub

You add a reference to an assembly by calling the References.Add method and passing the path to the assembly. The Add method copies the assembly into the project output folder unless a copy of the assembly with the same version and public key information is stored in the global assembly cache (GAC). This is done so that, when the project output is run or loaded by another assembly, the correct referenced assembly can be loaded. The following macro code adds a reference to an assembly:

Sub AddReferenceToAssembly()
    Dim vsproj As VSLangProj.VSProject
    Dim proj As EnvDTE.Project
    proj = DTE.Solution.Projects.Item(1)
    vsproj = CType(proj.Object, VSLangProj.VSProject)
    vsproj.References.Add("C:Program FilesCommon Files" & _
"Microsoft SharedMSEnvPublicAssembliesextensibility.dll")
End Sub

This code finds the VSProject object for a project and then adds a reference to the Extensibility.dll metadata assembly—the same assembly that contains the definition of the IDTExtensibility2 interface, which is used for building add-ins. You can't add assemblies located within the GAC as references to a project because the Visual Basic and Visual C# project systems maintain a separation between the files that are referenced for building against and files that are used during a component's run time.

During development, a component that is compiled by one project in a solution might be needed by a component in another project. You can create a reference from one project to another project by using the References.AddProject method. This method accepts a Project object and adds a reference to that project, as shown here:

Sub AddProjectReference()
    Dim vsproj As VSLangProj.VSProject
    Dim proj As EnvDTE.Project
    'Find the project the reference will be added to:
    proj = DTE.Solution.Projects.Item(1)
    vsproj = CType(proj.Object, VSLangProj.VSProject)
    'Find the referenced project:
    proj = DTE.Solution.Projects.Item(2)
    'Make the project to project reference:
    vsproj.References.AddProject(proj)
End Sub

Adding a reference to a COM object requires values that are COM-centric and might not be very intuitive to the non-COM programmer: the type library GUID, or library identifier (LIBID), of the type library that defines the COM component, and the version major and minor values of that type library. Using these values, you can add a reference to the type library of a COM component. When a reference to a COM library is made, the project system will first check to see if a primary interop assembly (PIA) exists for that COM object. If a PIA is found, the PIA will be added to the list of references. If a PIA is not found, the project will automatically create an interop assembly (IA) for that type library and then add a reference to that IA. The only difference between an IA and a PIA is a value in the system registry, PrimaryInteropAssemblyName, which is located underneath the LIBID and version number for a COM object's type library and points to the .dll file containing metadata describing the types the type library defines. Creating a PIA allows for one assembly file to define all types for one COM object, whereas there can be multiple IAs that handle this interop. If you plan on passing COM interfaces from one .NET program to another through a mechanism such as remoting, you should define a PIA because even though you could generate multiple IAs by wrapping the same type library, the types in one IA are considered distinct from the types in another IA, and an error will be generated. The following macro code adds a reference to the type library for Microsoft Windows Media® Player:

Sub AddCOMReference()
    Dim vsproj As VSLangProj.VSProject
    Dim proj As EnvDTE.Project
    proj = DTE.Solution.Projects.Item(1)
    vsproj = CType(proj.Object, VSLangProj.VSProject)
    vsproj.References.AddActiveX( _
        "{22D6F304-B0F6-11D0-94AB-0080C74C7E95}", 1, 0)
End Sub

Web References

The .NET Framework not only makes traditional software development easier, but it also makes new software development methodologies possible. One of these new methodologies involves XML Web services. XML Web services enable software development across the Internet by placing software code on a server, which can then be accessed by software that is run on the user's computer. Visual Studio makes connecting desktop software to XML Web services as easy as adding a Web reference. When a reference to an XML Web service is made, a special file written using the Web Services Description Language (WSDL) XML schema is downloaded from the server computer and a proxy class (a class that contains the logic to translate a method or property call from the client computer across the Internet to the server computer) is generated from the WSDL file. This proxy class can then be used to call to the XML Web service.

The following macro adds a Web reference to a project. It retrieves the VSProject object for a project and then calls the AddWebReference method with the URL for the XML Web service. This example uses the TerraServer Web service provided by Microsoft, which offers detailed geographic information and satellite images for the United States. This Web service is located at http://terraserver.microsoft.com/TerraService2.asmx

Sub AddTerraServerWebRef()
    Dim vsProj As VSLangProj.VSProject
    Dim serviceURL As String
    'Set the URL to the TerraServer web service
    serviceURL = "http://terraserver.microsoft.com/TerraService2.asmx"
    'Find the VSProject for a project
    vsProj = DTE.Solution.Projects.Item(1).Object
    'Add the web reference
    vsProj.AddWebReference(serviceURL)
End Sub

When this Web reference is made, the WSDL file describing the XML Web service is downloaded from the server computer, and the proxy class for the service is generated and automatically added to the project. This class is placed in a namespace defined by the Web protocol and server name, but in reverse order. So, for example, if the XML Web service were located at www.microsoft.com, the namespace for the service would be com.microsoft.www. In this example, TerraServer is located at the server URL terraserver.microsoft.com so the namespace used is com.microsoft.terraserver. Once a reference to an XML Web service has been added to a project, using that service is as easy as calling methods on the generated proxy class.

Imports

To make the programmer's life easier, Visual Basic and Visual C# source code can contain using and Imports statements to shorten the identifiers used to access the namespace defined by a library of code. For example, to display a message box, you could use the longer, more specific identifier to resolve to a class name:

System.Windows.Forms.MessageBox.Show("Hello World")

But if this code were repeated a number of times, you'd have to type the namespace identifier over and over, which could lead to programming errors. You can use an Imports statement in Visual Basic to shorten what you have to type:

Imports System.Windows.Forms

Later in the program, you can use this shorter form of the code:

MessageBox.Show("Hello World")

Visual Basic also allows you to enter Imports statements through a project's Properties window rather than by typing the Imports statement into the source code. By using the project properties dialog box instead of typing the statement into code, you can make the import available for all the files within the project, not just the file that uses the Imports statement. Using the VSProject.Imports collection, you can enumerate, remove, and add imports for the entire project. The following macro adds the System.XML namespace to a Visual Basic project:

Sub AddSystemXMLImport()
    Dim vsProj As VSLangProj.VSProject
    vsProj = DTE.Solution.Projects.Item(1).Object
    vsProj.Imports.Add("System.Xml")
End Sub

Note

The Imports object is valid only for the Visual Basic project type. Any attempt to access this object for a Visual C# or Visual J# project will return a null or Nothing value.

ProjectProperties

Each project has a number of options associated with it that allow you to control how you interact with that project. You can set these options under the Common Properties node of a project's Property Pages dialog box. The options include the name of the component that the compiler should build, the kind of project to be generated (an .exe or a .dll), and layout options for the HTML designer. You can also set these options programmatically by using the Properties property of the Project object. This property returns the same Properties object that's used throughout Visual Studio to set options. The following macro walks the list of properties available to a project as well as the values and types of each property:

Sub WalkVSProjectProperties()
    Dim project As EnvDTE.Project
    Dim properties As EnvDTE.Properties
    Dim [property] As EnvDTE.Property
    Dim owp As InsideVSNET.Utilities.OutputWindowPaneEx
    owp = New InsideVSNET.Utilities.OutputWindowPaneEx(DTE, _
        "Project properties")
    project = DTE.Solution.Projects.Item(1)
    properties = project.Properties
    For Each [property] In properties
        owp.WriteLine("Name: " + [property].Name)
        owp.WriteLine("Value: " + [property].Value.ToString())
        owp.WriteLine("Type: " + [property].Value.GetType().FullName)
        owp.WriteLine()
    Next
End Sub

You can use this Property object to read the values of properties and to set the properties for a project. The following macro demonstrates this. It sets the icon to use for a project when it is compiled. This code assumes that an icon named Icon.ico is located in the folder containing the project file.

Sub SetProjectIcon()
    Dim project As EnvDTE.Project
    Dim [property] As EnvDTE.Property
    Dim projectPath As String
    project = DTE.Solution.Projects.Item(1)
    'Get the Property object for the icon:
    [property] = project.Properties.Item("ApplicationIcon")
    'Construct the path to the icon based off of the
    ' project path:
    projectPath = project.FullName
    projectPath = System.IO.Path.GetDirectoryName(projectPath)
    projectPath = projectPath + "Icon.ico"
    'Set the icon for the project:
    [property].Value = projectPath
End Sub

Using Visual Studio Utility Project Types

To help you more easily maintain files within a solution, Visual Studio makes available various utility projects. These utility projects allow you to keep track of files that are not part of any other project that is loaded into a solution. Because any file type can be stored within these projects, such as program source files or Microsoft Word documents, these projects can't be compiled into a program. And because utility projects are not associated with any particular programming language, they are available to all users of Visual Studio and don't require Visual Basic, Visual C#, or Visual C++ to be installed.

Miscellaneous Files Project

When you're working with a solution, you might need to open files that are not part of an existing project. When you open such a file, it is automatically added to a project called Miscellaneous Files. A project file isn't created on disk for this project, as with other project types, but you get a convenient way of locating files that are open but are not part of any other project that is open within the solution. You can think of the Miscellaneous Files project as a list of most recently used open documents—when you open a file, an item for that file is added to the project, and when you close the file, it is removed. By default, the Miscellaneous Files project and the files it contains don't appear in the Solution Explorer tree hierarchy, but you can easily make them visible by opening the Tools Options dialog box, selecting the Environment | Documents node, and selecting the Show Miscellaneous Files In Solution Explorer check box.

The Miscellaneous Files project has a unique name associated with it that, unlike with other projects, doesn't change over time. This name, <MiscFiles>, is defined by the constant vsMiscFilesProjectUniqueName. The following macro retrieves the Project object for the Miscellaneous Files project:

Sub FindMiscFilesProject()
    Dim project As EnvDTE.Project
    Dim projects As EnvDTE.Projects
    projects = DTE.Solution.Projects
    project = projects.Item(EnvDTE.Constants.vsMiscFilesProjectUniqueName)
End Sub

When the first file is opened within the Miscellaneous Files project, an item is added to the Solution.Projects collection that implements the Project interface. It works just as the Project interface implemented by projects such as Visual Basic or Visual C#, except that a few of the properties will return null or Nothing or throw a System.NotImplementedException when called. Table 8-1 lists the methods and properties of the Project object that return a meaningful value for the Miscellaneous Files project and the ProjectItem and ProjectItems objects contained within this project.

Table 8-1. Methods and Properties of the Project, ProjectItems, and ProjectItem Objects

Project

ProjectItems

ProjectItem

DTE

DTE

DTE

ProjectItems

Parent

Collection

Name (read-only)

Item

Name (read-only)

UniqueName

GetEnumerator / _NewEnum

FileCount

Kind

Kind

Kind

FullName

Count

FileNames

 

ContainingProject

SaveAs

  

Save

  

IsOpen

  

Open

  

Delete

  

Remove

  

ExpandView

  

ContainingProject

  

IsDirty

You can add new files to the Miscellaneous Files project by using the ItemOperations.NewFile method, which has the following method signature:

public EnvDTE.Window NewFile(string Item = "GeneralText File",
    string Name = "", string ViewKind =
    "{00000000-0000-0000-0000-000000000000}")

By applying the techniques we used earlier to calculate the first parameter for the ItemOperations.AddNewItem method, we can find the value that should be passed to the NewFile method. The second parameter also has the same meaning as the second parameter of the ItemOperations.AddNewItem method—the name of the file (with extension) that is to be added—and if the empty string is passed, a default name is calculated. The last argument specifies which view the file should be opened in when it is added. These values can be found within the EnvDTE.Constants class and begin with the name vsViewKind.

Solution Folders

The Miscellaneous Files project lists files that are temporarily open in an editor, and when those files are closed, they are removed from that project. But what if you want to associate a file with a solution, not have that file built as part of a project, and have that file stay within your solution when the editor window for that file has been closed? Solution folders provide a way for you to satisfy these requirements and more. Solution folders can be created by right-clicking on the solution node within the Solution Explorer tool window, and choosing Add | New Solution Folder. Solution folders can also be created within existing solution folders by right-clicking on a solution folder and selecting Add | New Solution Folder. Because solution folders can be nested within one another, you can create a hierarchy of folders, each containing files on disk. Often I need to add a bunch of files to a solution that are not part of a project, and, to keep those files organized, I like to mirror the folder structure on disk with solution folders. Not only can you organize files on disk with solution folders, but you can also use them to organize projects. If your solution contains many projects, you can create a solution folder that contains, for example, all the Web applications in your project, one solution folder for all class libraries, etc. New or existing projects can be added to a solution folder from the context menu for a solution folder, or if the project you want to move is located under the solution node in the Solution Explorer tool window, you can just drag the project into the appropriate solution folder.

A Solution Folder also has a programmatic interface to this functionality. To create a solution folder within a solution, simply call the Solution2.AddSolutionFolder method, supplying a name for the new folder:

Sub CreateSolutionFolder()
    Dim solution2 As EnvDTE80.Solution2
    solution2 = CType(DTE.Solution, EnvDTE80.Solution2)
    solution2.AddSolutionFolder("My Folder")
End Sub

The AddSolutionFolder method returns an EnvDTE.Project object, which works as any other Project object, such as that available from Visual C# or Visual Basic projects, except you will find that many methods, such as the Save method, will not work for a solution folder because it has no meaning for that project type. The Object property on the Project interface for a solution folder returns an object that you can then cast into the interface EnvDTE80. SolutionFolder. The SolutionFolder interface also has a method named AddSolutionFolder, which will allow you to create a nested folder. This macro is a modified version of the one to add a folder to the solution, but it will also create a nested folder:

Sub CreateSolutionFolder()
    Dim solution2 As EnvDTE80.Solution2
    Dim project As EnvDTE.Project
    Dim solutionFolder As EnvDTE80.SolutionFolder
    solution2 = CType(DTE.Solution, EnvDTE80.Solution2)
    project = solution2.AddSolutionFolder("MyFolder")
    solutionFolder = CType(project.Object, EnvDTE80.SolutionFolder)
    solutionFolder.AddSolutionFolder("My Other Folder")
End Sub

The SolutionFolder interface will also allow you to programmatically add projects or files. The method AddFromFile will take the path to an existing project file (a file with an extension such as .csproj, .vbproj, or .vcproj), and add it to a solution folder. Here is an example of the use of this method, which will first create a solution folder named MyFolder and then will add an existing Visual C# project named ConsoleApplication to that solution folder:

Sub ProjectAdd()
    Dim project As EnvDTE.Project
    Dim solutionFolder As EnvDTE80.SolutionFolder
    Dim solution2 As EnvDTE80.Solution2

    solution2 = CType(DTE.Solution, EnvDTE80.Solution2)
    project = solution2.AddSolutionFolder("MyFolder")
    solutionFolder = project.Object
    solutionFolder.AddFromFile("C:ProjectConsoleApplication1.csproj")
End Sub

The AddFromTemplate method takes a path to a .vstemplate file, a .vsz file, or an existing project to clone; a path to store the project in; and a name of the new project to create. AddFromTemplate then generates a new project based upon the wizard or the existing project. This macro uses the GetProjectTemplate method of the Solution2 interface to find the path to the Visual C# Console Application template, and then creates a project based upon this template within a new solution folder.

Sub NewProjectAdd()
    Dim project As EnvDTE.Project
    Dim solutionFolder As EnvDTE80.SolutionFolder
    Dim solution2 As EnvDTE80.Solution2
    Dim CSConsoleTemplatePath As String

    solution2 = CType(DTE.Solution, EnvDTE80.Solution2)
    project = solution2.AddSolutionFolder("MyFolder")
    solutionFolder = project.Object

    CSConsoleTemplatePath = solution2.GetProjectTemplate _
           ("ConsoleApplication.zip", "CSharp")
    solutionFolder.AddFromTemplate(CSConsoleTemplatePath, _
           "C:ProjectsTestProject", "NewProject")
End Sub

Adding an existing or new file to a solution folder is done within the Project object by using the AddFrom methods, just as you would add an item to a Visual C# or Visual J# project.

Once you have a solution folder with items located within that folder, you can use the folder as a Project object to locate an item contained within it. For example, suppose you have a solution folder with a project loaded into it. The following macro finds the Project for the solution folder and then obtains the collection of ProjectItems for the solution folder. Even though a project within a solution folder is a project, it is still an item, and so the solution folder allows you to get to a list of ProjectItem objects by using the ProjectItems property. Once you have this ProjectItem object, you can call the Object property to retrieve the object specific to that project item node, which is a project.

Sub FindProjectInSolutionFolder()
    Dim slnFolderProject As Project
    Dim containedProject As Project
    Dim projectAsAProjectItem As ProjectItem

    'Find the solution folder. This code expects one folder and
    '  no other items within the solution
    slnFolderProject = DTE.Solution.Projects.Item(1)

    'Find the project item for the project in the folder
    projectAsAProjectItem = slnFolderProject.ProjectItems.Item(1)

    'Convert the project item into a Project
    containedProject = projectAsAProjectItem.Object
    MsgBox(containedProject.Name)
End Sub

This is the only way that you can get to a Project object for a project that is located within a solution folder. The Solution.Projects.Item method will not return a project's object for a project in a solution folder because the Projects.Item method looks only at the projects directly underneath the solution node in Solution Explorer.

If you have a reference to a Project object, either a project such as a Visual J# project or a Project object for a solution folder, and that project is contained within a solution folder, you can find the ProjectItem object of that project within the solution folder by using the ParentProjectItem property. This macro will find a Project object within a solution folder and then walk back up the hierarchy to find the solution folder that contains it.

Sub FindParentProject()
    'First, find the project nested in the solution folder:
    Dim nestedProject As Project
    Dim solutionFolder As Project

    solutionFolder = DTE.Solution.Projects.Item(1)
    nestedProject = solutionFolder.ProjectItems.Item(1).Object

    'Now, find the solution folder parent of the nested project
    Dim parentProjectItem As ProjectItem
    Dim parentProject As Project
    parentProjectItem = nestedProject.ParentProjectItem
    parentProject = parentProjectItem.ContainingProject

    'Make sure the parent project and the solution folder are the same
    MsgBox(parentProject.UniqueName = solutionFolder.UniqueName)
End Sub

Unmodeled Projects

All the project types we've discussed so far have implemented a Project object that can be used by a macro or an add-in. However, a few project types, such as a database project or a project that has been unloaded by using the Project | Unload Project command, don't implement the Project object themselves. To allow some programmability for these project types, Visual Studio supports the unmodeled project type. An unmodeled project provides an implementation of the Project object that supports only the properties common among all project types, which are DTE, Kind, and Name. All other properties and methods on this implementation of the Project object return values that have no useful meaning or generate an exception when called and shouldn't be used by a macro or an add-in. You can distinguish an unmodeled project from other project types by checking the Project.Kind property, which returns the constant EnvDTE.Constants.vsProjectKindUnmodeled if the project is an unmodeled project. The following macro enumerates all the projects loaded into a solution and determines which ones are unmodeled:

Sub FindUnmodeledProjects()
        Dim project As EnvDTE.Project
        For Each project In DTE.Solution.Projects
            If (project.Kind = EnvDTE.Constants.vsProjectKindUnmodeled) Then
                MsgBox(project.Name + " is unmodeled")
            End If
        Next
End Sub

Project and Project Item Events

Just as a solution fires events to allow an add-in or macro to respond to the actions the user is performing, the various project types also fire events so that an add-in or a macro can be informed of what the user is doing. You connect to the events fired by the different project types in different ways, but each project type supports the same interfaces used to handle the event invocations. Each project fires two classes of events: actions performed with the project and actions performed with the items within those projects. Here are the events and the signatures that are called when a project is added, removed, or renamed within a solution:

void ItemAdded(ByVal Project As EnvDTE.Project)
void ItemRemoved(ByVal Project As EnvDTE.Project)
void ItemRenamed(ByVal Project As EnvDTE.Project,
    ByVal OldName As String)

These are the signatures of events fired when a project item is added to, removed from, or renamed within a project:

void ItemAdded(ByVal ProjectItem As EnvDTE.ProjectItem)
void ItemRemoved(ByVal ProjectItem As EnvDTE.ProjectItem)
void ItemRenamed(ByVal ProjectItem As EnvDTE.ProjectItem, _
    ByVal OldName As String)

To connect to these events, you can use the Events2 object's ProjectItemsEvents and ProjectsEvents properties. These properties will return an instance of an EnvDTE. ProjectItemsEvents or EnvDTE.ProjectsEvents object, which you can then use to receive event notifications from all projects or project items open within a solution. As new projects are added to the solution, or as new items are added to a project, the event handlers for your implementation of these interfaces are called. To connect to one of these events from the Macros IDE, simply open the EnvironmentEvents module of a macro project, and in the left drop-down at the top of the EnvironmentEvents source code window, select either the ProjectsEvents or ProjectItemsEvents item. Then in the right drop-down, select the event that you want to connect to.

You can connect to these events for all projects open in a solution and you can connect to them on a project type—specific basis. This means that if you want to connect to events on just Visual Basic projects loaded into a solution, or just on Visual C# projects, you can. Rather than using the Events2.ProjectItemsEvents and Events2.ProjectEvents methods to connect to these language type—specific events, you can use the Events.GetObject method, passing in a special string indicating the project type you want to connect to. But first, you need to set up the event variables to handle the event. You declare the event variable within a macro project by adding the following code to the EnvironmentEvents module. (This example connects to the Visual C# project events.)

<System.ContextStaticAttribute()> _
Public WithEvents csharpProjectItemsEvents As EnvDTE.ProjectItemsEvents

When you enter this code, an entry appears in the left drop-down list at the top of the code for the EnvironmentEvents macro module. Select the entry to fill the right drop-down list with the events for this object, and select each event to create the code necessary for capturing that event. At this point, the event handler won't be invoked for Visual C# projects because the event variable, csharpProjectItemsEvents, has yet to be set to an instance of a ProjectItemsEvents object. To set this variable to an instance of the correct event object, create a handler for DTEEvents.OnStartupComplete and place within it the code to connect to the event, much as you would within an add-in:

Private Sub DTEEvents_OnStartupComplete() _
    Handles DTEEvents.OnStartupComplete

    csharpProjectItemsEvents = _
        DTE.Events.GetObject("CSharpProjectItemsEvents")
End Sub

With this event handler in place, when Visual Studio is closed and then restarted, the OnStartupComplete handler will be invoked, which will cause the event variable to be connected. Of course, you can insert this same code into a macro and run the macro; this way you will not need to restart Visual Studio for the event variable to be set. Here's an example of such a macro:

Sub ConnectCSharpProjectItemsEvents()
    csharpProjectItemsEvents = _
        DTE.Events.GetObject("CSharpProjectItemsEvents")
End Sub

You can connect to the project and project item events for project types other than the Miscellaneous File and Solution Items projects by changing the string passed to the Events.GetObject method. For example, to connect to Visual Basic project and project item events, you can use the strings VBProjectsEvents and VBProjectItemsEvents. You can use the strings VJSharpProjectsEvents and VJSharpProjectItemsEvents to connect to events thrown by a Microsoft Visual J# project, and you can use eCSharpProjectsEvents and eCSharpProjectItemsEvents to capture events thrown by a Visual C# smart device application. You can use eVBProjectsEvents and eVBProjectItemsEvents to capture events thrown by a Visual Basic smart device application. The ProjectEvents sample demonstrates how to connect to all these project and project item events. It connects to the events provided by each project type, and as each event is fired, a message box is displayed containing information about that event.

Managing Build Configurations

Editing and manipulating a project is an important part of the development process, but most of your time is spent building and compiling, not moving around files within a project. Visual Studio provides an object model for building a solution and controlling how the projects contained within that solution should be compiled. The root object for controlling how a solution should be built is named SolutionBuild; you access it by calling the Solution. SolutionBuild property, and you control how each project within the solution should be built by using the ConfigurationManager object, which is accessed through the Project. ConfigurationManager property.

Manipulating Solution Settings

Visual Studio uses solution configurations to manage how a solution is built. A solution configuration is a grouping of project configurations that describe how the projects within the solution should be built. A project configuration, in the simplest terms, tells the various compilers how to create the code for a project. Each project can contain multiple project configurations that you can switch between within the solution configuration to control how the compilers build the code. The most common solution and project configurations are debug and release, which cause a project to be built with debugging information and with code optimizations, respectively. When a Windows Forms project is first created, Visual Studio creates the debug solution configuration containing the Windows Forms debug project configuration and the release solution configuration containing the release Windows Forms project configuration. You can create new solution configurations that contain any of the available project configurations or new project configurations that can be loaded into any solution configuration.

SolutionConfiguration and SolutionContext Objects

Solution configurations are represented in the object model through the SolutionConfigurations collection, which contains SolutionConfiguration objects. Because the SolutionConfigurations object is a collection, you can use the standard techniques for enumerating this collection and use the Item method to find a specific SolutionConfiguration object by name. To create new solution configuration, you use the SolutionConfigurations.Add method, which makes a copy of an existing solution configuration and then renames it to the specified name. The signature of this method is

public EnvDTE.SolutionConfiguration Add(string NewName,
    string ExistingName, bool Propagate)

Here are the arguments that are passed to this method:

  • NewName This is the name of the new solution configuration. It can't be the same as any existing solution configuration name, and it must follow the file system's file-naming rules. (It can't contain characters such as , /, :, *, ?, ", <, or >.)

  • ExistingName This is either the name of an existing solution configuration that is copied to create the new solution configuration or the string "<Default>". If the name "<Default>" is used, the currently active solution configuration is used as the source of what is copied.

  • Propagate If this parameter is true, when the new solution configuration is created, a copy of each project configuration referenced by the solution configuration is made and assigned the same name as the new solution configuration, and each of these copies of project configurations is loaded into the new solution configuration. If this parameter is false, the new solution configuration is created and the same project configurations that were assigned to the solution configuration source are assigned to the new solution configuration.

The SolutionConfiguration object has one method, Activate, and one property of note, SolutionContexts. When a build is performed, whether through the user interface or through the object model by using the SolutionBuild.Build method, the currently active SolutionConfiguration is the configuration that is built. Therefore, activating a particular solution configuration by using the Activate method causes any build actions to build the active solution configuration. The other item of importance is the SolutionContexts property. As discussed earlier, a SolutionConfiguration is a container of the projects within a solution and the project configuration associated with that solution configuration. The SolutionConfiguration.SolutionContexts property returns a SolutionContexts collection, containing those projects and the configuration of each project to build.

To set the project configuration that is built when the solution is built, you can change the SolutionContext object's ConfigurationName to any project configuration name that the project supports. The following macro changes the debug solution configuration to build the release version of a project that is loaded into the solution:

Sub ChangeProjectConfiguration()
    Dim solutionBuild As EnvDTE.SolutionBuild
    Dim solutionCfgs As EnvDTE.SolutionConfigurations
    Dim solutionCfg As EnvDTE.SolutionConfiguration
    Dim solutionContext As EnvDTE.SolutionContext
    'Find the debug solution configuration:
    solutionBuild = DTE.Solution.SolutionBuild
    solutionCfgs = solutionBuild.SolutionConfigurations
    solutionCfg = solutionCfgs.Item("Debug")
    'Retrieve the solution context for the first project:
    solutionContext = solutionCfg.SolutionContexts.Item(1)
    'Change the debug solution context to build the
    ' Release project configuration:
    solutionContext.ConfigurationName = "Release"
    'Reset the build flag for this context:
    solutionContext.ShouldBuild = True
End Sub

You can modify a SolutionContext to set the project configuration that should be built for a particular solution configuration and you can also set values such as that specifying whether the project configuration should be built. This is done in the next-to-last line of the preceding macro, where the ShouldBuild property is set to true. In this macro, this property must be set because, as is expected, when the debug solution configuration is first created, it doesn't contain the release project configuration. It, therefore, isn't set to build for that solution configuration, so when the debug solution configuration is set to build the release project configuration, that "do not build" state is carried along with it.

StartupProjects

When you start a solution running (usually by pressing the F5 key), the project builder first verifies that all the projects that need to be built are up-to-date, and then it starts walking the list of projects that are set as startup projects, running each project in turn. You can set the list of startup projects through the user interface by right-clicking on the solution node in Solution Explorer and then choosing Set StartUp Projects from the shortcut menu. You'll see the Solution Property Pages dialog box (shown in Figure 8-3), in which you can set the startup projects for a solution containing four Windows Forms applications.

Setting the projects that will start when you run a solution

Figure 8-3. Setting the projects that will start when you run a solution

You can also set startup projects through the object model by using the SolutionBuild. StartupProjects property. This property is set to a value of type System.Object, which is packed with the projects to start when you run a solution. The value passed to the StartupProjects property can take two forms: a single string that is the unique name of a project (which will set one single project to run) or an array of System.Object (which will be filled with one or more project unique names and will cause multiple projects to be run).

For example, suppose an open solution contains two projects, each of them to be designated as a startup project. You can use code such as the following to set these projects as startup projects:

Sub SetStartupProjects()
    Dim startupProjects(1) As Object
    startupProjects(0) = DTE.Solution.Projects.Item(1).UniqueName
    startupProjects(1) = DTE.Solution.Projects.Item(2).UniqueName
    DTE.Solution.SolutionBuild.StartupProjects = startupProjects
End Sub

If only one project should be set as a startup project, the code looks like this:

Sub SetStartupProject()
    Dim startupProject As String
    startupProject = DTE.Solution.Projects.Item(1).UniqueName
    DTE.Solution.SolutionBuild.StartupProjects = startupProject
End Sub

When you set the startup projects, you must be careful to supply only buildable projects. If one of the projects supplied to SolutionBuild.StartupProjects is, for example, the unique name for the Miscellaneous Files project or the Solution Items project, an error is generated.

Project Dependencies

When you work with a solution that contains multiple projects, the components built by one project might rely on the output of another project. An example of this is a control project called UserControl, which is placed on the form of a Windows Forms application called WinForm. Because changes to the UserControl project might affect how that control is used by the Windows Forms project, the UserControl project must be compiled before the WinForm project is compiled. To enforce this relationship between the two projects, you can create a project dependency. The dependencies between two or more projects can be depicted using a dependency graph; the dependency graph for the projects WinForm and UserControl is shown in Figure 8-4. The arrow is pointing to the project that another project is dependent on.

A dependency graph showing a WinForm project dependent on a UserControl project

Figure 8-4. A dependency graph showing a WinForm project dependent on a UserControl project

Suppose we add a new project to the solution—a class library called ClassLib that implements functionality used by both the WinForm and the UserControl projects. A dependency graph for this solution is shown in Figure 8-5.

The dependency graph for three projects

Figure 8-5. The dependency graph for three projects

You can see in this dependency graph that the WinForm project can't be built until the UserControl and ClassLib projects have been built. The UserControl project relies on only the ClassLib project being built first. When a build of this solution is started, if the build system chooses the UserControl project to start building first, the ClassLib project builds. If the build system chooses the ClassLib project first, because it does not have any dependencies, it can build immediately without needing to build any other projects. When the UserControl project is built, the ClassLib project isn't built again because it is up-to-date. Because it relies upon the output of the other two projects, the last project to be built is the WinForm project because it relies on the output of the other two projects.

A problem can occur with a dependency graph if you create a cyclic dependency, in which one or more projects are mutually dependent. Suppose the WinForm project relies on the UserControl project, the UserControl project relies on the ClassLib project, and the ClassLib project relies on the WinForm project. The cycle shown in Figure 8-6 is generated.

A dependency graph of three projects with a cycle

Figure 8-6. A dependency graph of three projects with a cycle

If the WinForm project is built, the build of the UserControl project is triggered because of the dependency. Building the UserControl project causes the building of the ClassLib project, which is dependent on the WinForm project. If the Visual Studio build system were unable to detect this cycle, the loop would continue forever in an attempt to find the first project to build. But Visual Studio is smart enough to detect dependency cycles, and it disallows them.

You can create dependencies between projects through the user interface by choosing Project | Project Dependencies, which will display the Project Dependencies dialog box (shown in Figure 8-7). The dialog box shows all the projects that can be set as a dependency for the UserControl project. The WinForm check box is shaded because a dependency is set from the WinForm project to the UserControl project, and Visual Studio won't allow a cycle between the WinForm project and the UserControl project to be created.

You can also set build dependencies through the object model. The SolutionBuild. BuildDependencies property returns a BuildDependencies object, which is a collection of BuildDependency objects. You can index this collection by using the Item method—you can pass a numeric index, an EnvDTE.Project object, or the unique name of a project. Each project in the solution has its own EnvDTE.BuildDependency object, whose RequiredProjects property you can use to add, remove, or retrieve dependencies for a project. The following macro displays in the Output window the available projects in the open solution, as well as all the projects it depends on.

Setting project dependencies

Figure 8-7. Setting project dependencies

Sub Depends()
    Dim projectDep As EnvDTE.BuildDependency
    Dim project As EnvDTE.Project
    Dim owp As New InsideVSNET.Utilities.OutputWindowPaneEx(DTE,
        "Build dependencies")

    For Each projectDep In DTE.Solution.SolutionBuild.BuildDependencies
        Dim reqProjects As Object()

        owp.Write("The project ")
        owp.Write(projectDep.Project.Name)
        owp.WriteLine(" relies on:")
        reqProjects = projectDep.RequiredProjects
        If (reqProjects.Length = 0) Then
            owp.WriteLine(vbTab + "<None>")
        Else
            For Each project In reqProjects
                owp.WriteLine(vbTab + project.Name)
            Next
        End If
        owp.WriteLine()
    Next
End Sub

Using the BuildDependency object, you can create a macro or an add-in that sets up the dependencies between two or more projects. Suppose, using our current example, that a solution with the projects WinForm, UserControl, and ClassLib is loaded and no dependencies have been set. The BuildDependency object supports three methods for modifying the projects that a project is dependent on: AddProject, RemoveProject, and RemoveAllProjects. AddProject and RemoveProject accept the unique name of a project that should be added or removed as a dependency for a specific project. RemoveAllProjects takes no arguments and removes all project dependencies. The following macro, SetDependencies, builds the correct dependencies for the three-project solution to conform to the dependency graph shown in Figure 8-5:

Sub SetDependencies()
    Dim buildDependencies As EnvDTE.BuildDependencies
    Dim buildDependency As EnvDTE.BuildDependency
    Dim project As EnvDTE.Project

    Dim winFormUniqueName As String
    Dim userControlUniqueName As String
    Dim classLibUniqueName As String

    'Gather up the unique name of each project
    For Each project In DTE.Solution.Projects
        If (project.Name = "WinForm") Then
            winFormUniqueName = project.UniqueName
        ElseIf (project.Name = "UserControl") Then
            userControlUniqueName = project.UniqueName
        ElseIf (project.Name = "ClassLib") Then
            classLibUniqueName = project.UniqueName
        End If
    Next

    buildDependencies = DTE.Solution.SolutionBuild.BuildDependencies
    For Each buildDependency In buildDependencies
        If (buildDependency.Project.Name = "WinForm") Then
            buildDependency.RemoveAllProjects()
            'Add all projects except the WinForm
            ' project as a dependency:
            buildDependency.AddProject(userControlUniqueName)
            buildDependency.AddProject(classLibUniqueName)
        ElseIf (buildDependency.Project.Name = "UserControl") Then
            buildDependency.RemoveAllProjects()
            'Add a dependency to the ClassLib project:
            buildDependency.AddProject(classLibUniqueName)
        End If
    Next
End Sub

Manipulating Project Settings

Solution configurations are used to group together project configurations. Each project contains a number of configurations that control how the compiler should create the program code for that project. Because a project can have multiple project configurations associated with it, you can generate different versions of a program.

ConfigurationManager Object

You manage project configurations through the ConfigurationManager object, which has a collection of Configuration objects and lets you create new configurations. Configurations for a project are arranged in a grid pattern, with the configuration type, such as debug or release, along one axis of the grid and the platform on which the configuration will be built for on the other axis. The platforms that Visual Studio currently supports are Win32® for 32-bit Microsoft Windows® running on the x86 processor and Any CPU (the configuration name Any CPU is given to programs that will run on the .NET Framework), if the project is being compiled for the Microsoft .NET platform—including .NET applications for the desktop or smart device. Because projects can build only one platform type at a time, the second axis will always have one dimension.

Note

Previous versions of Visual Studio used the name .NET for the platform name when compiling to MSIL bytecode. If you are upgrading an add-in or macro to Visual Studio, you will need to change your code to use the new name, Any CPU.

You can find a particular project configuration in several ways. The first way is to use the familiar Item method that's available on all collection objects. However, unlike most other Item methods on collection objects, the ConfigurationManager.Item method requires two parameters. The first parameter can be a numerical index and spans the entire grid of platforms and configurations. You can also use Item to directly locate a Configuration by passing the configuration name as the first parameter and the platform name as the second parameter. Suppose a Visual C++ project is open in Solution Explorer. To find the Configuration object for the Win32 debug build, you can use code such as the following:

Sub RetrieveDebugWin32Configuration()
    Dim config As Configuration
    Dim project As EnvDTE.Project
    project = DTE.Solution.Projects.Item(1)
    config = project.ConfigurationManager.Item("Debug", "Win32")
End Sub

Another way to retrieve specific configurations is to use the ConfigurationManager. ConfigurationRow and ConfigurationManager.Platform methods, which take the build type and the platform name, respectively. These methods return a collection of Configuration objects that you can iterate through to find a specific item. The ConfigurationRow method returns a list of all configurations with the passed name; the Platform method returns a list of all configurations belonging to a specific platform. These methods are most useful if you want to modify the settings of configurations that are closely related to one another, such as walking all the Win32 configurations of a Visual C++ project and enabling managed extensions, thus allowing your program to use the .NET Framework in C++ code. The following code sample does just that. After finding the Win32 configurations available to a project, it retrieves the Properties object for that configuration and sets the ManagedExtension property to true, allowing the compiler to generate code that can work with the .NET Framework.

Sub SetManagedExtensionsProperty()
    Dim configManager As ConfigurationManager
    Dim configs As Configurations
    Dim config As Configuration
    Dim project As EnvDTE.Project
    project = DTE.Solution.Projects.Item(1)
    configManager = project.ConfigurationManager
    configs = configManager.Platform("Win32")
    For Each config In configs
        Dim prop As EnvDTE.Property
        prop = config.Properties.Item("ManagedExtensions")
        prop.Value = True
    Next
End Sub

You can create new configurations based on an existing configuration in the same way that you can create new solution configurations by copying an existing solution configuration. You create new project configurations by using the ConfigurationManager. AddConfigurationRow method. This method takes as its parameters the name of the new configuration and an existing configuration name, which is used as a template for creating the new configuration. AddConfigurationRow also accepts as an argument a Boolean value. This parameter, named Propagate, works in the same way as the Propagate parameter of the SolutionConfigurations.Add method, but in reverse. When the SolutionConfigurations.Add method is called with the Propagate parameter set to true, a copy of the solution configuration and all the project configurations it contains is made. If the AddConfigurationRow method is called with its Propagate parameter set to true, the currently active solution configuration is copied, its name is set to the name passed as the new project configuration, and the new solution configuration is modified to contain the newly created project configuration.

Note

The ConfigurationManager object contains the method AddPlatform, which works much the same as the AddConfigurationRow method but adds a platform row to the build type configuration grid. If you call this method for any of the current versions of the Microsoft-language products, an exception will be generated because new platforms can't be added for these project types. This doesn't mean that this method won't work for third-party programming language projects or future versions of Microsoft programming languages.

Most project types support only one platform type, but some projects, such as setup projects, are not associated with any platform—what is built is platform-agnostic. A setup project doesn't care whether its contents are intended for Win32 or .NET platforms; its role is to contain files to be installed onto the user's computer, so a platform is not a consideration when you build a setup project. Because the build type configuration grid can't be one-dimensional, a pseudoplatform is generated for these project types, and its name is set to <N/A>.

Project Configuration Properties

Project configurations differ in the property values that are set. For example, one difference between the debug and release configurations is that the debug configuration doesn't optimize the code, which makes debugging easier to perform, and optimization is turned on for the release configuration to make the code run faster. Such properties are set through the object returned by calling the Configuration.Properties property. As you saw earlier in the SetManagedExtensionsProperty macro example, this property returns an EnvDTE.Properties object—the same object that is used throughout Visual Studio to set property values on various objects. The following macro retrieves the debug and release configurations for a project, reads the Boolean Optimize configuration property, negates it, and then stores it back into the configuration. This means that the Optimize property is inverted for all these configurations.

Sub SwapOptimizationSettings()
    Dim project As EnvDTE.Project
    Dim configManager As EnvDTE.ConfigurationManager
    Dim configs As EnvDTE.Configurations
    Dim config As EnvDTE.Configuration
    Dim props As EnvDTE.Properties

    'Find the ConfigurationManager for the project:
    project = DTE.Solution.Projects.Item(1)
    configManager = project.ConfigurationManager

    'Get the debug configuration manager
    configs = configManager.ConfigurationRow("Debug")
    'Walk each configuration in the debug configuration row
    For Each config In configs
        Dim optimize As Boolean
        'Get the Optimize property for the configuration
        props = config.Properties
        optimize = props.Item("Optimize").Value
        'Negate the value
        props.Item("Optimize").Value = Not optimize
    Next

    'Repeat for the release configuration
    configs = configManager.ConfigurationRow("Release")
    For Each config In configs
        Dim optimize As Boolean
        'Get the Optimize property for the configuration
        props = config.Properties
        optimize = props.Item("Optimize").Value
        'Negate the value
        props.Item("Optimize").Value = Not optimize
    Next

End Sub

Build Events

As each stage of a build is performed, Visual Studio fires an event that can be captured by a macro or add-in, allowing custom code to be run. Four events are defined. Here are their signatures:

void OnBuildBegin(EnvDTE.vsBuildScope Scope, EnvDTE.vsBuildAction Action);
void OnBuildProjConfigBegin(string Project, string ProjectConfig,
    string Platform, string SolutionConfig);
void OnBuildProjConfigDone(string Project, string ProjectConfig,
    string Platform, string SolutionConfig, bool Success);
void OnBuildDone(EnvDTE.vsBuildScope Scope, EnvDTE.vsBuildAction Action);

These event handlers have the following meanings:

  • OnBuildBegin This event is fired just before a build is started. Two arguments are passed to the handler of this event. The first argument is an enumeration of type EnvDTE.vsBuildScope, which can be either vsBuildScopeBatch (if you chose to start a batch build of one or more projects), vsBuildScopeProject (if you selected a single project to build by right-clicking a project and choosing Build), or vsBuildScopeSolution (if you chose the active solution configuration to build). The second argument is of type EnvDTE.vsBuildAction and can be either vsBuildActionBuild (if the project or solution configuration is to be compiled), vsBuildActionClean (if the project or solution configuration's build output is to be deleted from disk), vsBuildActionDeploy (if the project or solution configuration is to be deployed to its target), or vsBuildActionRebuildAll (if the project or solution configuration is to be rebuilt, even if the project's dependencies do not warrant a rebuild).

  • OnBuildProjConfigBegin This event is fired when a project's configuration starts to be built. It is passed four arguments, each of type string. The first argument is the unique name of the project being built, the second is the name of the configuration being built, the third is the name of the platform being built, and last is the name of the solution configuration being built.

  • OnBuildProjConfigDone This event handler is fired after a project configuration has been built. It is passed the same arguments as the OnBuildProjConfigBegin event, with the addition of a Boolean value that signals whether the configuration was built successfully (true) or failed to build (false).

  • OnBuildDone This event is fired after all build steps have been completed, whether successfully or unsuccessfully.

Among the samples that accompany this book is one called BuildEvents, which demonstrates connecting to each of the build events. As each event handler is called, the information passed to that event handler is displayed within the output window, which contains information about the arguments that were passed to each handler. For example, if we were to create a solution containing two projects, ClassLibrary1 and ClassLibrary2, load the sample add-in, and perform a build on the solution by choosing Build | Build Solution, the following information would be displayed:

OnBuildBegin
    Scope: vsBuildScopeSolution
    Action: vsBuildActionBuild

OnBuildProjConfigBegin
    Project: ClassLibrary1.csproj
    Platform: Any CPU
    Solution Configuration: Debug

OnBuildProjConfigDone
    Project: ClassLibrary1.csproj
    Platform: Any CPU
    Solution Configuration: Debug
    Success: True

OnBuildProjConfigBegin
    Project: ..ClassLibrary2ClassLibrary2.csproj
    Platform: Any CPU
    Solution Configuration: Debug

OnBuildProjConfigDone
    Project: ..ClassLibrary2ClassLibrary2.csproj
    Platform: Any CPU
    Solution Configuration: Debug
    Success: True

OnBuildDone
    Scope: vsBuildScopeSolution
    Action: vsBuildActionBuild

This output outlines the steps performed to build this two-solution project. It starts with a call to the OnBuildBegin event handler and then builds each project configuration contained within the solution configuration, one after another, with the OnBuildDone event handler being fired to signal that the build process has been completed. With Visual Studio, the OnBuildProjConfigBegin and OnBuildProjConfigEnd events are fired one after another, with no other build events fired between them. However, a macro or add-in should not take advantage of this order of events if you plan to port this code to a future version of Visual Studio because future versions might take advantage of multiprocessor computers, building one project configuration on one processor and another project configuration on another processor. If a macro or an add-in were to rely on this order of events, the code might not work properly.

Persisting Solution and Project Information Across IDE Sessions

At times, your add-in or macro might need to save some data that should be carried along with the solution or project file. The object model supports saving information into these files with the EnvDTE.Globals object. You can find this object by calling the Globals property of both of these objects:

Sub SolutionGlobals()
    Dim globals As EnvDTE.Globals
    globals = DTE.Solution.Globals
End Sub

Sub ProjectGlobals()
    Dim globals As EnvDTE.Globals
    globals = DTE.Solution.Projects.Item(1).Globals
End Sub

The Globals object of the Solution and Project objects works in much the same way as the Globals object found on the DTE object, with a few minor differences. First, if a macro or an add-in stores data into the solution or project file, even if the VariablePersists flag is set for that variable, the data might not be written into the solution or project file. This is because making a change to a variable causes the project or solution file to be put into a modified state. If you close the solution or project file but do not choose to save the modified files, the data won't be written into that file. Second, unlike the EnvDTE.Globals object on the DTE object, which can store data in a wide variety of formats, data stored into a solution or project file can be stored only in string format. This is because project and solution files are text-based, so any data stored into these files must also be in a text format. This doesn't mean that nonstring data can't be stored into the solution or project Globals object. It just means that when the data is to be written into the solution or project files, an attempt will be made to convert the data into a string. If that fails, the data won't be stored. Also, because the data is converted into a string when it is stored into the solution or project files, when the Globals object is restored from the solution or project file, this data will also be in a string format. It is up to the macro or add-in code to properly determine which format the data is in.

A good use of the Globals object is to keep track of the number of times you build a project. I like to count the number of times I build a project. Not that this number has any significance, but it is just an interesting fact. The following macro sample is an implementation of the OnBuildDone event. As each OnBuildDone event is fired, the sample checks for the existence of the BuildCounter variable within the solution Globals object. If this value exists, it is incremented and stored back into the Globals object. If this value doesn't exist, the value 1 is stored. The code for the OnBuildDone event is shown here:

Private Sub BuildEvents_OnBuildDone(ByVal Scope As _
    EnvDTE.vsBuildScope, ByVal Action As EnvDTE.vsBuildAction) _
    Handles BuildEvents.OnBuildDone
    'Increment the build counter by storing a value in the
    ' solution file through the Globals object:
    Dim globals As Globals
    Dim int32 As System.Int32
    globals = DTE.Solution.Globals
    If (globals.VariableExists("BuildCounter")) Then
        'A counter has been set, increment it:
        int32 = System.Int32.Parse _
           (globals.VariableValue("BuildCounter").ToString())
        int32 = int32 + 1
        globals.VariableValue("BuildCounter") = int32.ToString()
        globals.VariablePersists("BuildCounter") = True
    Else
        'The variable has never been set, seed the counter:
        globals.VariableValue("BuildCounter") = 1.ToString()
        globals.VariablePersists("BuildCounter") = True
    End If
End Sub

Looking Ahead

In this chapter, we looked at how the pieces of the object model fit together to programmatically manage the many project types that can be loaded into a solution. In the next chapter, we will see how to program the user interface elements of Visual Studio, such as the many different tool and document windows.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset