Cameron McColl, Bill Horst
Visual Basic Product Team
Microsoft Corporation
August 2004
Summary: This paper describes some of the interesting properties and quirks of the Visual Studio .NET Visual Basic project model. The paper explores what's going on, and proposes possible workarounds. ( printed pages)
Applies to:· Microsoft Visual Studio .NET 2003· Microsoft Visual Studio 2002.Introduction
Consider having several hundred assemblies that are all consumed by a MAIN project either directly or indirectly. Clearly there is no need to open a solution containing all of these projects. Instead you decide to use file references for any assembly which you are not modifying. This makes managing the solution simpler and improves performance. For assemblies that you do need to modify you add the relevant project to your solution and make project references instead of file references.For example, in one particular case, a Visual Studio user had written an extensive macro that automated the creation of the solution along with the correct projects and references.The Problem: Mixing File and Project References
Suppose you have a solution with the following three projects:
Figure 1: Sample SolutionThis configuration causes the following error in Visual Basic if Sub Main uses the type X from Project A:Project 'Main' makes an indirect reference to assembly 'ProjectB', which contains 'class2'. Add a reference to 'ProjectB' to your project.The Explanation: Indirect References
Why does this error occur? The key to the problem is mentioned in the error message–"indirect reference."Before considering the definition of an indirect reference, you should be clear on the difference between a file reference and a project reference.A file reference is a reference to the built assembly of a project. In the above figure, the file reference to Project A is actually a reference to the assembly Project A produced as a result of building the project. This assembly is usually located in the project's Bin directory.A project reference is a reference to the actual project. You can think of this as an in-memory representation of the assembly, which is automatically updated as you edit your code.So, what is an indirect reference? In the example, Project Main calls X.Bar in its Sub Main. To compile this code, the compiler needs to resolve the name X. The definition of X is found in Project A, which Main references directly. The compiler must also resolve the name Bar. Since the type ofX is in Project B the compiler looks to see if it has a reference to Project B. By design, the compiler does not consider a project reference when resolving a type that is defined in a file referenced assembly (such as Project A). Thus the compiler does not see the Main project reference to Project B. The compiler does know, however, that assembly A references assembly B. Since Main references A, the compiler sees assembly B as an indirect reference. Thus the compiler reports back that there needs to be a direct reference to Project B for this code to work. This means a File Reference to Project B.Workarounds
The main problem stems from mixing file and project references within the same solution. These are some of the possible workarounds and the problems you may encounter with these:· Always use project references--This implies all projects live in the same solution. However, this workaround becomes hard to manage when dealing with large numbers of projects within a given solution, or when some of the projects are built in a language other than Visual Basic, such as C#. A Project Reference to a C# project may look the same as a Project Reference to a Visual Basic project however the C# reference is always a File Reference.· Always use file references--If you decide to do this, you always add a reference to a project by selecting the dll from the output Bin directory. This is done for each project that is in your solution. For assemblies that you do not include in your solution, you can use a common directory to host these. This works because these assemblies are not changing with successive builds of your solution.Note This approach has issues that need to be handled carefully to avoid problems. To understand these problems you must first be familiar with the Reference Path property of Visual Basic projects. See below for more detail.· Build your project outputs to a common Bin directory and use CopyLocal=False--It may seem to make sense to always use file references, but avoid the CopyLocal=True problem. In this scenario, all projects in your solution build to a common directory and your reference path for each project is to this same common directory. Unfortunately, there is a known issue with this approach: Assemblies located in this common directory get locked if they exceed 64K in size. Once an assembly is locked by a referencing project, other projects are unable to build against these references. See Microsoft Knowledge Base Article – 313512!href(http://support.microsoft.com/default.aspx?scid=kb;en-us;313512) for details on how to make this a viable model for your project development.Understanding the Reference Path property
Suppose you have two projects, Project A and Project B. You decide that Project B requires a reference to Project A, so you navigate to the Bin directory for Project A and select the Project A .dll. If you now view the properties for Project B, notice that the Reference Path property setting contains an entry that is the full path to the Project A Bin directory.The Reference Path property tells Visual Studio where to look for referenced assemblies; the default is the path you used when you added the reference. Thus, when you build Project B, Visual Studio takes each path listed in the Reference Path property and looks to see if it contains the assembly being sought. In this example, Visual Studio is looking for the Project A.dll. If the assembly is not found, Visual Studio looks at the next Reference Path setting, and so on. If the assembly is found, Visual Studio copies the assembly from the found location into the Bin directory of the project being built. This is what the CopyLocal=True property means. This ensures that you always have the latest copy of the assembly at runtime.So, Visual Studio uses the Reference Path property during compilation; it should not be confused by the algorithm used to locate an assembly at run time.Sounds good so far, so where do the problems start? Suppose that in addition to your Projects A and B, there are also a number of additional assemblies that Project A references but you do not have as projects in your solution. For example, you might place the latest version of all your assemblies in a common directory. You now decide that Project B needs to add a reference to Assembly X in your common directory. When you add the reference you are presented with the following message box:
Figure 2: Message box reporting problem.What is going on here? When you add this reference Visual Studio attempts to add the directory c:\common (your common assembly directory) to the reference path for this project. When it does this, the path is placed at the beginning of the reference path ordering. Visual Studio uses this ordering to locate an assembly at compile time. If the common directory contains an assembly that was previously being picked up by a path further down the reference path list, Visual Studio now picks this assembly from the common directory instead of the previous location. The dialog box is simply warning you of this change.So, when you see this message box what do you do? If you click No, your reference is not added, clearly not what you wanted. If you click Yes, your reference is added but there are side effects. In this particular scenario the side effect is quite serious. The Project B reference to Project A was previously being copied from the Project A Bin directory, but Visual Studio now takes it from the common directory. This is bad because changes you make to Project A code are not now reflected in Project B. This is because the copy of the Project A .dll in the common directory is not updated when you build Project A.To remedy this, make sure that your Reference Path settings are in the correct order to achieve the desired effect. In this particular scenario, where you have a common directory for assemblies that are not included as projects in the current solution, place the common directory as the last entry in the Reference Path property settings to get the correct behavior.Macro Solution
Of course, doing this every time you change a project reference is time consuming and tedious. You may find the following macro a useful way to keep your reference path value correct.Note Using this macro is only advised when conditions are as follows:1. You always use file references even if the assembly is build by a project currently in your solution.2. If an assembly is being built by a project in your solution, any references to this assembly should be made from the project’s bin directory.3. All assemblies not being built by projects in your solution should be located in a common assembly directory and all references to these assemblies should be made from this directory.If these conditions are being met the macro updates the reference path property for each project in your solution and modifies it to ensure that the original problem described in this paper does not occur.Run this macro after adding or removing references in the projects of your solution. It has the following assumptions:· There are no references to assemblies in the bin directories of projects not included in this solution.· A .dll (dynamic-link library) file is only present in a project's bin directory if it is referenced by the project.· No two projects in the solution have the same name, and that no project references an assembly outside the solution with the same name as a project inside the solution.' Name: FileReferenceMacro' Author: William Horst [WHorst@Microsoft.com]' Purpose: The purpose of this macro is to avoid problems that can' occur with mismatched symbols when combining project and file' references. The code walks through all the projects in the currently' opened solution and replaces all references with file references. ' It then updates the reference paths so that all project bin' directories are checked before any other folders, and' that a project's bin directory is checked before any other path with ' its assembly present.
Imports EnvDTEImports System.DiagnosticsImports System.IOImports System.CollectionsImports System.Collections.Specialized
Public Module FileReferenceMacro ' This is the name of the log file. It will be stored in the C ' directory. Const LogFileName As String = "ProjectRef.lst" ' Purpose: This is the main method of the macro and carries out the ' behavior described above. Sub SetFileRefsForDevelopment() Dim logPath As String = "C:\" & LogFileName Dim FileOut As StreamWriter Dim Name As String Dim proj As EnvDTE.Project Dim Projects As SortedList Dim refproj As EnvDTE.Project Dim vsproject As VSLangProj.VSProject FileOut = New StreamWriter(logPath) ' Create collection of all project names Projects = New SortedList For index As Integer = 1 To DTE.Solution.Projects.Count proj = DTE.Solution.Projects.Item(index) If proj.UniqueName <> EnvDTE.Constants.vsMiscFilesProjectUniqueName Then Projects.Add(proj.Name, index) End If Next index
' Iterate through all projects in the solution. For Each proj In DTE.Solution
FileOut.WriteLine("* * * * * * * * * * * * * * * * * * * * * * *") FileOut.WriteLine("Processing project " & proj.Name) vsproject = CType(proj.Object, VSLangProj.VSProject)
' Don't try to process the special project for misc files. If proj.UniqueName <> EnvDTE.Constants.vsMiscFilesProjectUniqueName Then ' Change all references to file references. For Each ref As VSLangProj.Reference In vsproject.References Name = ref.Name
' If in solution: If Projects.ContainsKey(Name) Then
refproj = ref.SourceProject
' If Not Project reference: If refproj Is Nothing Then
' Grab project. For Each project As EnvDTE.Project In DTE.Solution If project.Name = ref.Name Then refproj = project End If Next End If
' Add reference to project bin directory. Dim dllFolder As String = refproj.FullName.Substring(0, refproj.FullName.LastIndexOf("\"c) + 1) & "bin\" Dim dllpath As String = dllFolder & refproj.Name & ".dll"
' Remove old reference and add new one. If ProjectExists(dllpath) Then
FileOut.WriteLine("Removing reference " & ref.Path) ref.Remove()
FileOut.WriteLine("Adding reference " & dllpath) ref = vsproject.References.Add(dllpath) ' Check if bin directory is in the reference path already. Dim oldRefPath As String = CStr(proj.Properties.Item("ReferencePath").Value) If Not (oldRefPath.Trim.ToUpper.EndsWith(dllFolder.ToUpper) Or (oldRefPath.ToUpper.IndexOf(dllFolder.ToUpper & ";")) > -1) Then ' If not, add bin directory to the reference path. If Not oldRefPath.Trim.EndsWith(";"c) Then dllFolder = ";" & dllFolder proj.Properties.Item("ReferencePath").Value &= dllFolder End If Else ' Break out of macro. FileOut.WriteLine(dllpath & " does not exist - build project and re-run macro") GoTo Endmacro End If End If Next ref
' Update reference paths. Dim binDirRefPaths As New ArrayList Dim newrefpath As String = "" Dim otherRefPaths As New ArrayList Dim projPath, projpath2 As ProjectPath Dim refPaths As String() = CStr(proj.Properties.Item("ReferencePath").Value).Split(";"c)
FileOut.WriteLine("Updating reference paths") FileOut.WriteLine("Old paths:")
' Divide into project bin directories and other paths. For Each path As String In refPaths
projPath.project = Nothing projPath.text = path FileOut.WriteLine(path)
' Determine if this path is a project's bin directory. For Each project As EnvDTE.Project In DTE.Solution If path.ToUpper.Trim.StartsWith((project.FullName.Substring(0, project.FullName.LastIndexOf("\"c) + 1) & "bin").ToUpper) Then projPath.project = project Exit For End If Next
If projPath.project Is Nothing Then otherRefPaths.Add(path) Else binDirRefPaths.Add(projPath) End If Next
' Make sure a project's bin directory comes before any other bin directories which contain the same assembly. For index As Integer = 0 To binDirRefPaths.Count - 1
projpath2 = binDirRefPaths(index)
' For each path before this path: For subindex As Integer = 0 To index - 1
projPath = binDirRefPaths(subindex)
' If the path contains this assembly, move this path in front of it. If Dir(AppendBackslash(projPath.text) & projpath2.project.Name & ".dll") <> "" Then binDirRefPaths.RemoveAt(index) binDirRefPaths.Insert(subindex, projpath2) End If
Exit For Next Next
' Add bin directory paths together into a new path. For Each projPath In binDirRefPaths If newrefpath = "" Then newrefpath = projPath.text Else newrefpath += ";" & projPath.text End If Next
' Add other paths to the new path. For Each path As String In otherRefPaths If newrefpath = "" Then newrefpath = path Else newrefpath += ";" & path End If Next
' Give the new set of reference paths to the project. proj.Properties.Item("ReferencePath").Value = newrefpath FileOut.WriteLine("New Path: " & newrefpath)
End If Next proj
GoTo EndMacroEndMacro: ' Show log. FileOut.WriteLine("Exiting") FileOut.Close() Shell("Notepad " & logPath, AppWinStyle.NormalFocus)
End Sub
' Purpose: This method determines whether the project exists and ' displays an error message if it does not. ' Argument: ProjName is the name of the project for which to check.
Private Function ProjectExists(ByVal ProjName As String) As Boolean If Dir(ProjName) = "" Then MsgBox(ProjName & " doesn't exist - build the project and then re-run the macro") Return False Else Return True End If End Function
' Purpose: This method appends a "\"c to the end of the string passed ' in, if it does not end in that character already. ' Argument: The string to append. Private Function AppendBackslash(ByVal str As String) As String If str.EndsWith("\"c) Then Return str Else Return str & "\"c End If End Function
' Purpose: This structure is an easy way to store a project bind ' directory path and the accompanying project.
Private Structure ProjectPath Public text As String Public project As EnvDTE.Project End Structure
End ModuleConclusion
Microsoft recognizes that these problems exist and is working hard to remedy these in future releases of Visual Studio .NET.Further Reading
How to: Add a Project Referencems-help://MS.MSDNQTR.v80.en/MS.MSDN.v80/MS.VisualStudio.v80.en/dv_vsintro/html/3bd75d61-f00c-47c0-86a2-dd1f20e231c9.htmManaging Visual Studio Projectsms-help://MS.MSDNQTR.v80.en/MS.MSDN.v80/MS.VisualStudio.v80.en/dv_vsintro/html/983f3c18-832f-4666-afec-74b716ff3e0e.htm
Source fromhttp://blogs.msdn.com/b/vbteam/archive/2004/07/14/183403.aspx
Visual Basic Product Team
Microsoft Corporation
No comments:
Post a Comment