Introduction
Visual Studio 2005 / 2008 both lack a good way to define assembly references per solution configuration (‘Debug’ or ‘Release’).
Background
There are some ways to define assembly references which change if the solution configuration changes, but they are not sufficient for larger projects. The following features of Visual Studio 2005 / 2008 are fine for small projects:
- Set a Project reference from one project to another. The projects must be all in one solution. This is not always possible in projects where a lot of developers are working on multiple projects lying in different solutions.
- Set a reference to an assembly lying in the output path of the project. Visual Studio 2005 searches for assembly references first in the output path of the project. A way to define assembly references per solution configuration is to put all assemblies in a common output directory (MSDN Article). This is inflexible.
The Lexware Assembly Reference Tool fills this gap, by providing a new tool window in Visual Studio 2005 / 2008, which allows you to change ‘hard-coded’ assembly reference paths to flexible reference paths which change depending on the solution configuration. The tool detects ‘Debug’ or ‘Release’ in the assembly reference path and marks the assembly in red to show you the potential problem you have, when you build the project in another configuration. You only need to press a button and all these paths will be converted to paths which depend on the configuration of the project.
Additionally, the tool provides the following features:
- Change the ‘Copy Local’ property
- Manipulate the ‘Special Version’ property
- Edit the assembly reference path (‘
HintPath
’)
- Convert a project reference into an assembly references
- Delete an assembly reference
After installing and starting the Add-In, you can open the assembly reference tool via the Visual Studio 2005 / 2008 tools menu.
How It Works
The General Idea Behind It
The tool allows flexible assembly reference paths by replacing “\Debug\” or “\Release\” in the reference path against “\$Configuration\”, which is a placeholder for the solution configuration in Visual Studio 2005 / 2008. Visual Basic and CSharp projects are able to replace the placeholder back to “\Debug\” or “\Release\” when trying to resolve an assembly path.
Since the Visual Studio 2005 / 2008 object model doesn't allow any change to a reference path, the tool changes the path in the underlying project file. It manipulates the tag, the path to the assembly, which is referenced.
When the tool saves the project file, Visual Studio notices that change and asks you to reload the project.
For some features, the tool holds a reference to the internal Visual Studio representation of an assembly reference (VSLangProj80.Reference3
), which allows a manipulation of properties like ‘CopyLocal
’, ‘SpecificVersion
’ or deleting a reference. Changes to these properties are directly reflected by the original property window of the assembly reference in Visual Studio.
The Tool Solution Itself
The solution contains a C# and a setup project. The setup project was created with the Setup Project template. The C# project is originally created with a Visual Studio Add-In project template of Visual Studio 2005.
Visual Studio automatically creates a ‘Tool’ menu item for the add-in, if you choose so in the project wizard. If you select ‘I would like my Add-In to load, …’ Visual Studio loads your Add-In directly when it starts.
In the new project, Visual Studio creates two files with the extension ‘AddIn’. One is lying in the project folder (e.g. ‘Lexware.Tools.AssemblyReferences.AddIn
’) and this is used when the Add-In is deployed. The other one (e.g. ‘Lexware.Tools.AssemblyReferences
- for Testing.AddIn
’) is placed in the Visual Studio Add-In folder. This is the one which is used while debugging the Add-In. The Visual Studio Add-In folder is contained in your documents folder. For Windows Vista, it should be found here: C:\Users\’Your User Name’\Documents\Visual Studio 2005\Addins. An Add-In file contains a full description of the Add-In. It provides a friendly name, the load behavior and other information, necessary for Visual Studio to display the Add-In in the ‘Add-in Manager’.
The Add-In
The Add-In project contains the file ‘connect.cs’, which contains the code connecting to Visual Studio. The class ‘Connect
’ is called, when the Add-In starts. In fact, it is defined in the ‘FullClassName
’ tag of the ‘.Addin
’ file described below.
The Class Connect
The class Connect
implements the Visual Studio interface IDTExtensibility2
, which defines the method OnConnection
. This method is called when Visual Studio loads the Add-In into its memory. This is the place where you can add menu items and in this case, create a tool window.
public void OnConnection(object application, ext_ConnectMode connectMode,
object addInInst, ref Array custom)
{
_applicationObject = (DTE2)application;
_addInInstance = (AddIn)addInInst;
CreateToolWindow();
if(connectMode == ext_ConnectMode.ext_cm_UISetup)
{
object[] contextGUIDS = new object[] {};
Commands2 commands = (Commands2)_applicationObject.Commands;
string toolsMenuName;
try
{
ResourceManager resourceManager = new ResourceManager(
"Lexware.Tools.AssemblyReferences.CommandBar",
Assembly.GetExecutingAssembly());
CultureInfo cultureInfo = new CultureInfo(_applicationObject.LocaleID);
string resourceName = String.Concat(cultureInfo.TwoLetterISOLanguageName,
LocalResources.ToolbarName);
toolsMenuName = resourceManager.GetString(resourceName);
}
catch
{
toolsMenuName = LocalResources.ToolbarName;
}
Microsoft.VisualStudio.CommandBars.CommandBar menuBarCommandBar = (
(CommandBars)_applicationObject.CommandBars)["MenuBar"];
CommandBarControl toolsControl = menuBarCommandBar.Controls[toolsMenuName];
CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl;
try
{
Command command = commands.AddNamedCommand2(_addInInstance,
"AssemblyReferences", "Assembly Reference Tool",
"Checks and fixes assembly references.
Uses placeholder for debug and release directory.",
true, 0, ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported +
(int)vsCommandStatus.vsCommandStatusEnabled,
(int)vsCommandStyle.vsCommandStyleText,
vsCommandControlType.vsCommandControlTypeButton);
if((command != null) && (toolsPopup != null))
{
command.AddControl(toolsPopup.CommandBar, 1);
}
}
catch(ArgumentException)
{
}
}
}
When the user clicks your menu item, the method Exec
in the same class will be called. You can filter the commandName
, to know whether the called menu item was yours.
public void Exec(string commandName, vsCommandExecOption executeOption,
ref object varIn, ref object varOut, ref bool handled)
{
handled = false;
if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
{
if(commandName == "Lexware.Tools.AssemblyReferences.Connect.AssemblyReferences")
{
CreateToolWindow();
handled = true;
return;
}
}
}
The tool window, which contains all the important code in this Add-In, is a UserControl
. To create it, you can call CreateToolWindow2
. This method creates a new Visual Studio tool window and hosts a user control in it.
private void CreateToolWindow()
{
if(_toolWindow != null)
{
_toolWindow.Activate();
}
else
{
Windows2 windows2 = (Windows2)_applicationObject.Windows;
Assembly asm = Assembly.GetExecutingAssembly();
object customControl = null;
string className = "Lexware.Tools.AssemblyReferences.ToolWindowControl";
string caption = "Assembly References";
_toolWindow = windows2.CreateToolWindow2(_addInInstance, asm.Location, className,
caption, _toolWindowGuid,
ref customControl);
try
{
_toolWindow.SetTabPicture(LocalResources.LexwareBmp.GetHbitmap());
}
catch
{
}
_toolWindow.Visible = true;
if (customControl != null)
{
_toolWindowControl = (ToolWindowControl)customControl;
_toolWindowControl.ApplicationObject = _applicationObject;
_toolWindowControl.ParentToolWindow = _toolWindow;
}
}
}
The ToolWindow
The tool window registers some events of the Visual Studio solution, document and the command object, so that changes in the solution will be noticed by the Add-In.
private void RegisterEvents()
{
if (_solutionEvents != null)
{
UnregisterEvents();
}
_solutionEvents = _applicationObject.Events.SolutionEvents;
_solutionEvents.Opened += new _dispSolutionEvents_OpenedEventHandler(
_solutionEvents_Opened);
_solutionEvents.ProjectAdded += new _dispSolutionEvents_ProjectAddedEventHandler(
_solutionEvents_ProjectAdded);
_solutionEvents.ProjectRemoved += new _dispSolutionEvents_ProjectRemovedEventHandler(
_solutionEvents_ProjectRemoved);
_solutionEvents.ProjectRenamed += new _dispSolutionEvents_ProjectRenamedEventHandler(
_solutionEvents_ProjectRenamed);
_solutionEvents.AfterClosing += new _dispSolutionEvents_AfterClosingEventHandler(
_solutionEvents_AfterClosing);
_documentEvents.DocumentSaved += new _dispDocumentEvents_DocumentSavedEventHandler(
_documentEvents_DocumentSaved);
_commandEvents.AfterExecute += new _dispCommandEvents_AfterExecuteEventHandler(
_commandEvents_AfterExecute);
}
private void _commandEvents_AfterExecute(string Guid, int ID, object CustomIn,
object CustomOut)
{
if (((Guid == "{5EFC7975-14BC-11CF-9B2B-00AA00573819}") && (ID == 331)) ||
((Guid == "{5EFC7975-14BC-11CF-9B2B-00AA00573819}") && (ID == 224)) ||
((Guid == "{5EFC7975-14BC-11CF-9B2B-00AA00573819}") && (ID == 226)))
{
ReadAllReferences();
}
else if ((Guid == "{1496A755-94DE-11D0-8C3F-00C04FC2AAE2}") && (ID == 1113))
{
ParentToolWindow.Activate();
}
}
When the solution changes, the Add-In iterates the projects contained in the solution and reads in the references of each project. It adds each reference to the list view and puts an instance of the class AssemblyReferenceInformation
at the tag of the ListViewItem
. This instance contains information about the project file (.csproj or .vbproj) and the assembly reference (VSLangProj80.Reference3
), which will be used when manipulating the reference.
private void ReadAllReferences()
{
ClearHintLists();
if ((_applicationObject != null) && (_applicationObject.Solution != null))
{
foreach (Project currentProject in _applicationObject.Solution.Projects)
{
ReadProjectReferences(currentProject);
}
}
}
private void ReadProjectReferences(Project currentProject)
{
try
{
if (currentProject != null)
{
VSProject2 visualStudioProject = currentProject.Object as VSProject2;
if (visualStudioProject != null)
{
string projectFullName = currentProject.FullName;
if (!string.IsNullOrEmpty(projectFullName))
{
FileInfo projectFileInfo = new FileInfo(projectFullName);
if (projectFileInfo.Exists &&
((projectFileInfo.Extension == _csProjectFileExtension) ||
(projectFileInfo.Extension == _vbProjectFileExtension)))
{
ListViewGroup projectGroup = GetProjectGroup(currentProject);
AddAssemblyHintsToListView(currentProject, projectFullName,
projectGroup, visualStudioProject);
}
}
}
else if ((currentProject.ProjectItems != null) && (
currentProject.ProjectItems.Count > 0))
{
foreach (ProjectItem currentProjectItem in currentProject.ProjectItems)
{
if (currentProjectItem.SubProject != null)
{
ReadProjectReferences(currentProjectItem.SubProject);
}
}
}
}
toolStripButtonFixIt.Enabled = (_needsToBeSaved.Count > 0);
}
catch (Exception ex)
{
ShowMessage(ex);
}
}
When the user hits the 'FixIt' button, the tool changes the underlying project file. It saves all projects which need to be saved, due to an incorrect assembly path.
private void SaveDirtyProjects()
{
try
{
foreach (KeyValuePair<string> project in _needsToBeSaved)
{
XmlDocumentHolder documentHolder = project.Value;
SaveProject(documentHolder);
}
}
catch (Exception ex)
{
ShowMessage(ex);
}
}
private void SaveProject(XmlDocumentHolder documentHolder)
{
string projectName = documentHolder.Project.FullName;
if (!documentHolder.Project.Saved)
{
MessageBox.Show("Please save all projects before you fix the problems.");
return;
}
if ((_sourceControl != null) && (_sourceControl.IsItemUnderSCC(projectName)) &&
(!_sourceControl.IsItemCheckedOut(projectName)))
{
_sourceControl.CheckOutItem(projectName);
}
using (XmlTextWriter writer = new XmlTextWriter(documentHolder.ProjectFileName,
Encoding.UTF8))
{
writer.Formatting = Formatting.Indented;
documentHolder.XmlDocument.Save(writer);
}
}
Visual Studio notices that the tool changed the project file and asks you to reload the project.
The Setup Project
The setup project deploys the Add-In to the AddIns folders of Visual Studio 2005 and Visual Studio 2008.
It automatically detects newer and older version, older versions will be removed before installing a new version. To create a new setup version for the Add-In, only change the version of the setup. Visual Studio will generate a new ProductCode
for you, but it will not touch the UpgradeCode
. With the combination of codes, Windows Installer is able to detect and update older or newer versions of the Add-In.
Requirements to Build the Solution
To build the solution, you need Visual Studio 2008. The Add-In is tested on Visual Studio 2005 and 2008 Team Developer and Team Suite.
History
We ported the source code of the Add-In to Visual Studio 2010. Visual Studio 2010 introduces the concept of extensions. For extensions, the deployment is much easier than for Visual Studio 2005 / 2008, so that the MSI setup is not needed anymore. The rest of the code remains at least the same. The new code is hosted at http://assemblyreftool.codeplex.com, the extension for Visual Studio 2010 can be downloaded from the Visual Studio Code Gallery.