Inside Microsoft® Visual Studio® .NET 2003 [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Inside Microsoft® Visual Studio® .NET 2003 [Electronic resources] - نسخه متنی

Brian Johnson

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید










Working with Solutions


In Visual Studio .NET, 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 one.

Within the Visual Studio .NET 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 .NET 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 filename 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 the SaveAs method can be used. 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 not only create the solution but also save it:


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, you won't be notified that the current solution is being closed, even if the current solution has been modified. This means 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 select files to save and returns a value detailing which button you clicked. This property also saves the files you've chosen to save. This property won't show the dialog box if no files need to be savedit 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 solutionthe only piece missing from the life cycle of a solution is closing 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 .NET 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, like the Item method of other objects does, 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 .NET might eventually support loading two projects that have the same name but are located in different folders on disk. (Visual Studio .NET 2003 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 version of Visual Studio .NET after version 7.1, 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 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 .NET object model team, after performing usability studies, found that developers didn't recognize the Solution object as a collection. They therefore added this Projects collection to help developers find the list of projects more easily. The EnumProjects macro can be rewritten as follows to 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 Solution Explorer tree view window. You can do this using the DTE.ActiveSolutionProjects property. When you call this property, Visual Studio .NET 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 using 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


Capturing Solution Events


As you interact with a solution, Visual Studio .NET 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 wayby 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 handlerthe EnvDTE.Project object for the project that was inserted.


void ProjectRenamed(EnvDTE.Project Project, string Old­Name)

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 .NET begins to close a solution file. The handler for this event is passed one argumenta 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 closedusers 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 Source code for the solution events add-in


SolutionEvents.cs
namespace SolutionEvents
{
using System;
using Microsoft.Office.Core;
using Extensibility;
using System.Runtime.InteropServices;
using EnvDTE;
[GuidAttribute("1FF0C203-8036-4A54-A71A-B0F82BA60B0C"),
ProgId("SolutionEvents.Connect")]
public class Connect : Object, Extensibility.IDTExtensibility2
{
public Connect()
{
}
public void OnConnection(object application,
Extensibility.ext_ConnectMode connectMode,
object addInInst, ref System.Array custom)
{
applicationObject = (_DTE)application;
addInInstance = (AddIn)addInInst;
//Set the solutionEvents delegate variable using the
// DTE.Events.SolutionEvents property:
solutionEvents = (EnvDTE.SolutionEvents)
applicationObject.Events.SolutionEvents;
//Set up 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(
Extensibility.ext_DisconnectMode disconnectMode,
ref System.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 System.Array custom)
{
}
public void OnStartupComplete(ref System.Array custom)
{
}
public void OnBeginShutdown(ref System.Array custom)
{
}
//SolutionEvents.AfterClosing delegate handler:
public void AfterClosing()
{
System.Windows.Forms.MessageBox.Show(
"SolutionEvents.AfterClosing",
"Solution Events");
}
//SolutionEvents.BeforeClosing delegate handler:
public void BeforeClosing()
{
System.Windows.Forms.MessageBox.Show(
"SolutionEvents.BeforeClosing",
"Solution Events");
}
//SolutionEvents.Opened delegate handler:
public void Opened()
{
System.Windows.Forms.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)
{
System.Windows.Forms.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)
{
System.Windows.Forms.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)
{
System.Windows.Forms.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(System.Windows.Forms.MessageBox.Show(
"SolutionEvents.QueryCloseSolution\nContinue with close?",
"Solution Events",
System.Windows.Forms.MessageBoxButtons.YesNo) ==
System.Windows.Forms.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)
{
System.Windows.Forms.MessageBox.Show(
"SolutionEvents.Renamed\nNew solution name: " +
applicationObject.Solution.FullName + "\nOld solution name: "
+ oldName, "Solution Events");
}
private _DTE applicationObject;
private AddIn addInInstance;
//The delegate handler variable:
private EnvDTE.SolutionEvents solutionEvents;
}
}


Lab: Bug with Events 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 due to 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 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 outside the method and to 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.


Solution Add-ins


As you saw in Chapter 6, Visual Studio .NET lets you write customization code by creating add-ins. Once you load an add-in, it will continue to run until you unload it or Visual Studio .NET is closed. Just as Visual Studio .NET can load and run add-ins, solutions can do so as well. As Visual Studio .NET starts to load a solution, it examines the solution file to see whether it contains a reference to any add-ins. If it does, it loads those add-ins and calls the same methods on the IDTExtensibility2 interface (OnConnection, OnDisconnection, and so forth) as appropriate, just as if the add-in were loaded as a nonsolution add-in.

Creating a solution add-in can offer some benefits over creating a non­solution add-in, such as when an add-in should be available only when a specific solution is open and running. For example, suppose you want to keep track of how many times a build of a solution is performed. You could create an add-in that would be loaded in the traditional way, but if the add-in were to count the number of times a specific solution is built, having this add-in loaded all the time would waste system resources.

The Add-in Wizard doesn't offer any options for creating solution add-ins, but by creating a nonsolution add-in and making a few modifications, you can generate all the necessary basic code. The only difference between a nonsolution add-in and a solution add-in is in how Visual Studio .NET is told to load the add-in. A standard add-in stores information in the registry; solution add-ins store their information directly in the solution (.sln) file. Knowing this, you can easily change the output for a standard add-in into a solution add-in. The first step is to run the Add-in Wizard, selecting the appropriate language but not changing any of the other options from their default. After the wizard finishes running and generating code, you delete the setup project for that add-in project. The setup project is used mainly to populate the system registry with values, and because these values aren't needed for a solution add-in, the setup project isn't necessary.

Note

If you follow these steps to create a solution add-in, this add-in will be available for loading as a nonsolution add-in on your development computer because the wizard creates the registry keys for a standard add-in when the code is generated. Having these extra keys in the system registry might be a little awkward for the developer of the solution add-in, but it won't cause any unwanted effects for the user of the solution add-in. If you do not want to clutter the Add-in Manager dialog box or if you want to remove the possibility of accidentally loading the solution add-in as a nonsolution add-in, you can safely remove these keys.

The next step is to register the add-in with the solution file. The property Solution.AddIns returns an AddIns collectionthe same one that is returned from the DTE.AddIns property except that this collection contains only the add-ins registered with the solution file. To register the add-in with the solution, you use the AddIns.Add method, which has the following method signature and parameters:


public EnvDTE.AddIn Add(string ProgID, string Description, string Name,
bool Connected)

Here's what the arguments that are passed to this method mean:


ProgID

The COM ProgID of the add-in to be associated with the solution.


Description

A description of the add-in. This value is used only for note keeping and isn't used by Visual Studio .NET.


Name

A short name for the add-in. Like the description, this value isn't used by Visual Studio .NET.


Connected

If this value is set to true, the add-in will be loaded after being associated with the solution and will be loaded again whenever the solution is reopened. If this value is false, the add-in will be associated with the solution but won't be loaded, and it won't be loaded when the solution is reopened.


Because you can't associate an add-in with a solution file through the registryyou must use codeyou must decide on a way of running the code to insert the add-in into the solution. If it is running in a shared environment in which the source code is checked into a source code control system (such as Microsoft Visual SourceSafe), the project administrator can write and run a macro with the appropriate code and then check in the solution file, thus making the add-in load for everyone the next time the latest version of the solution is retrieved from source code control. Another approach is to create a wizard that makes the appropriate call to AddIns.Add after generating the code for the project. The easiest approach, however, might be to use something similar to the Add-in Manager dialog box to manage solution add-ins.

Visual Studio .NET doesn't provide a solution add-in manager, but you can build one using an add-in (a standard add-in, not a solution add-in). The SolutionAddinManager add-in, which is among the book's sample files, adds a command to the shortcut menu for the solution node in Solution Explorer. Choosing this command displays a dialog box in which you can load, unload, add, and remove solution add-ins.


/ 118