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 a project, not moving around files within a project. Visual Studio .NET 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 .NET 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 project such as a Windows Forms project is first created, Visual Studio .NET 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 and one property of note. One of these methods is named Activate; 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 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 the list of those projects and which configuration of each project to build.
To set which project configuration 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
Not only can you modify a SolutionContext to set the project configuration that should be built for a particular solution configuration, but 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 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-4), in which you can set the startup projects for a solution containing four Windows Forms applications.
Figure 8-4. Setting the projects that will start when you run a solution
You can also set startup projects through the object model 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, both 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.
Note
Visual Studio .NET contains a bug that affects using the SolutionBuild.StartupProjects property with multiple projects. When you change this property from starting only one project to starting multiple projects, the list of startup projects is modified. However, the Multiple Startup Projects option button won't be selected; you must select it yourself. Setting the SolutionBuild.StartupProjects property won't affect this button's state.
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-5. The arrow is pointing to the project that another project is dependent on.
Figure 8-5. A dependency graph showing a WinForm project dependent on a UserControl project
Suppose we add a new project to the solutiona 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-6.
Figure 8-6. 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 only on 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, this causes the ClassLib project to build. If the build system chooses the ClassLib project first, that project is built immediately because it doesn't depend on any other projects. When the UserControl project is built, the ClassLib project isn't built again because it is up-to-date. Regardless of which project the build system chooses to build first, 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-7 is generated.
Figure 8-7. 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 .NET 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 .NET 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-8). 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 .NET won't allow a cycle between the WinForm project and the UserControl project to be created.
Figure 8-8. Setting project dependencies
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 using the Item methodyou 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.
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-6:
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 .NET currently supports are Win32 for 32-bit Windows running on the x86 processor, .NET if the project is being compiled for the Microsoft .NET platform, and Pocket PC and Windows CE if the project is built for the Windows CE platforms. Because projects can build only one platform type at a time, the second axis will always have one dimension.
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 other Item methods on most 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 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 like 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, and some projects, such as setup projects, do not support any platformwhat 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 objectthe same object that is used throughout Visual Studio .NET 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 .NET 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 on 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 built 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: .NET
Solution Configuration: Debug
OnBuildProjConfigDone
Project: ClassLibrary1.csproj
Platform: .NET
Solution Configuration: Debug
Success: True
OnBuildProjConfigBegin
Project: ..\ClassLibrary2\ClassLibrary2.csproj
Platform: .NET
Solution Configuration: Debug
OnBuildProjConfigDone
Project: ..\ClassLibrary2\ClassLibrary2.csproj
Platform: .NET
Solution Configuration: Debug
Success: True
OnBuildDone
Scope: vsBuildScopeSolution
Action: vsBuildActionBuild
The information above 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 .NET 2003, 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 .NET 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.