Tips and Tricks of Visual Studio File and Project References

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 Solution

This 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 of X 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 EnvDTE

Imports System.Diagnostics

Imports System.IO

Imports System.Collections

Imports 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 EndMacro

EndMacro:

        ' 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 Module

Conclusion

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.htm

Managing Visual Studio Projectsms-help://MS.MSDNQTR.v80.en/MS.MSDN.v80/MS.VisualStudio.v80.en/dv_vsintro/html/983f3c18-832f-4666-afec-74b716ff3e0e.htm