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 Documents\Visual Studio 2005\ProjectTemplates 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 Settings\username\Application Data\Microsoft\ VisualStudio\8.0\ProjectTemplatesCache. 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, _             "Microsoft\VisualStudio\8.0\ProjectTemplatesCache</quote>)     '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 Basic\MyTemplate.zip\MyTemplate.vstemplate</quote>) 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(strin g 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.Projec t 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.Projec t Project, strin g 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 Listing 8-1.

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

image from book
 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\nProject: " +  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\nProject: " +  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\nProject: " +  project.UniqueName + "\nOld 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\nContinue 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\nNew solution name: " + applicationObject.Solution.FullName + "\nOld solution name: " + oldName, "Solution Events");          }         private DTE2 applicationObject;         private AddIn addInInstance;         //The delegate handler variable:         private EnvDTE.SolutionEvents solutionEvents;     } } 
image from book

image from book
Is It a Bug When My Events Are Being Disconnected?

Over the years, I've often been asked if there is a bug with events because events can be unexpectedly lost and no longer fire, even if code to disconnect an event is never run. This problem is because of a common programming mistake that reveals itself because of how the garbage collector works in the .NET Framework. Look at the following code, which connects to the solution's Renamed event:

 public void ConnectSolutionEvents() {     EnvDTE.SolutionEvents solutionEvents;      solutionEvents = (EnvDTE.SolutionEvents)      applicationObject.Events.SolutionEvents;     solutionEvents.Renamed += new         _dispSolutionEvents_RenamedEventHandler(Renamed); } 

When this method is called to connect to the Renamed event, the solutionEvents variable is assigned to an instance of the SolutionEvents object. But the solutionEvents variable is local to the ConnectSolutionEvents method and, as a result, when ConnectSolutionEvents returns to the caller, solutionEvents is marked as available to be garbage collected. Usually the event fires once or twice, but when the garbage collector starts working, it sees that this variable can be removed from memory and removes it, thus disconnecting the event handler. To make your event handler code work correctly, you should move the solutionEvents variable declaration outside the method and to the class scope. This will ensure that the event handler isn't collected until the class is unloaded. Also, note that this behavior applies to all event handlers when they're connected using the .NET Framework, not just the Solution Renamed event.

image from book




Working with Microsoft Visual Studio 2005
Working with Microsoft Visual Studio 2005
ISBN: 0735623155
EAN: 2147483647
Year: 2006
Pages: 100

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net