Visual Studio 2010 DTE: How to make added DLL reference absolute and not copied
Summary:
We need to duplicate the behaviour of the Add Reference dialog, using DTE, when you add a specific DLL (it adds a Hint path entry to the reference in the CSProj file).
**Note: There is another related, but not duplicated, post from me here: https://stackoverflow.com/questions/6690655/visual-studio-2010-add-in-how-to-get-a-references-hint-path-property Please also read that one for more information about this issue. I have now added a decent bounty to get an answer to this and will happily spread up-votes over any decent answers :)*
The story so far:
I am converting a project reference to a direct DLL reference programmatically using DTE.
Assuming I have a simple solution with a Project2
(the parent project) which references a Project1
(the child project), I make the change like this:
project1Reference = FindProjectReference(project2.References, project1);
project1Reference.Remove();
Reference dllReference = project2.References.Add(project1DllPath);
where project1DllPath refers to the "c:\somewhere\Project1\Bin\Debug\Project1.dll"
file.
The problem I cannot yet solve is that the new reference is not to
"c:\somewhere\Project1\Bin\Debug\Project1.dll"
but instead points to
"c:\somewhere\Project2\Bin\Debug\Project1.dll"
(and the file is copied there).
If I add the DLL directly/manually using the Add Reference menu, it does not do this copying.
How do I add a DLL reference to an existing project's DLL without it taking a copy and referencing that instead?
I have tried adding dllReference.CopyLocal = false;
after the Add but aside from setting the flag it made no difference. There appear to be no options to modify the path after creation.
Update: I have also tried programmatically removing any Build dependency on Project1 from Project2, but that had no effect.
Below is the difference between the csproj files:
As a project:
<ItemGroup>
<ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj">
<Project>{86B3E118-2CD1-49E7-A180-C1346EC223B9}</Project>
<Name>ClassLibrary1</Name>
</ProjectReference>
</ItemGroup>
As a DLL reference (path was lost completely):
<ItemGroup>
<Reference Include="ClassLibrary1, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
...
</ItemGroup>
As a manually referenced DLL:
<ItemGroup>
<Reference Include="ClassLibrary1, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\ClassLibrary1\bin\Debug开发者_如何学C\ClassLibrary1.dll</HintPath>
</Reference>
...
</ItemGroup>
It looks like being able to specify the hint path for the DLL reference is the key. How do you set a hint path on a DLL reference (assuming you only have a handle to the Reference property)?
More information (20 July 2011):
The suggestion from Muse VSExtensions below does not impact the DLLs in question, as a copy has already been made from the DLL's project BIN to the parent project's BIN folder. The parent project does not bother to make use of the reference path as it already has the child DLL in its output folder.
Also the Reference Paths
of a project are saved to the project.csproj.user file and not to the project.csproj file.
I'm convinced that this is a new bug/feature in VS 2010 because I have an add-in that started showing similar behaviour a couple of days ago after I migrated it from VS 2008... Basically, if you add a reference to anything within VS's assembly search path, it will be added without the path hint.
I managed to find other VS add-ins that solved this problem (Power Tools, NuGet among others), and they all seem to use MsBuild. I don't know if MsBuild raises resource usage much - I myself haven't seen too big a slowdown, possibly because References.Add() is very slow to start with. But note that to get an instance of MsBuild project, a method called "GetLoadedProjects" is used, which may mean that it works on data already present in memory.
Below is the code I used to fix my add-in, it's a simplified version of what I found on the net... Essentially, the idea is to add the reference as usual, then use MsBuild to set the path hint. Setting the hint is a trivial operation, but finding the instance of the MsBuild project item to add the hint to is incredibly complicated. I tried to hack an alternative using MsBuild only, but ran into other problems... This one seems to work.
One other thing that may be of interest here: the code contains a sort of optimization - it doesn't add the hint to the new reference if the reference's path is equal to the path we wanted to add. This is good enough for the case in question, and detects correctly when VS decides to use the dll from the output folder instead of what we told it. But when I tried to add a reference to the dll already in the output folder (I use a single output folder for many related projects), the add-in didn't set the hint path and project seemed to switch to using some other dll in the path (in my case the one from its PublicAssemblies folder)... So it may be useful to remove that "if (!newRef.Path.Equals(..." line altogether and add the hint always. I'm still investigating this case, so any additional - uhm, hints or improvements of the code are welcome.
string newFileName = "the path to your.dll";
VSLangProj.VSProject containingProject = yourProject;
VSLangProj.Reference newRef;
newRef = containingProject.References.Add(newFileName);
if (!newRef.Path.Equals(newFileName, StringComparison.OrdinalIgnoreCase))
{
Microsoft.Build.Evaluation.Project msBuildProj = Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.GetLoadedProjects(containingProject.Project.FullName).First();
Microsoft.Build.Evaluation.ProjectItem msBuildRef = null;
AssemblyName newFileAssemblyName = AssemblyName.GetAssemblyName(newFileName);
foreach(var item in msBuildProj.GetItems("Reference"))
{
AssemblyName refAssemblyName = null;
try
{
refAssemblyName = new AssemblyName(item.EvaluatedInclude);
}
catch {}
if (refAssemblyName != null)
{
var refToken = refAssemblyName.GetPublicKeyToken();
var newToken = newFileAssemblyName.GetPublicKeyToken();
if
(
refAssemblyName.Name.Equals(newFileAssemblyName.Name, StringComparison.OrdinalIgnoreCase)
&& ((refAssemblyName.Version != null && refAssemblyName.Version.Equals(newFileAssemblyName.Version))
|| (refAssemblyName.Version == null && newFileAssemblyName.Version == null))
&& (refAssemblyName.CultureInfo != null && (refAssemblyName.CultureInfo.Equals(newFileAssemblyName.CultureInfo))
|| (refAssemblyName.CultureInfo == null && newFileAssemblyName.CultureInfo == null))
&& ((refToken != null && newToken != null && Enumerable.SequenceEqual(refToken, newToken))
|| (refToken == null && newToken == null))
)
{
msBuildRef = item;
break;
}
}
}
if (msBuildRef != null)
{
Uri newFileUri = new Uri(newFileName);
Uri projectUri = new Uri(Path.GetDirectoryName(containingProject.Project.FullName).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
Uri relativeUri = projectUri.MakeRelativeUri(newFileUri);
msBuildRef.SetMetadataValue("HintPath", relativeUri.ToString());
}
}
Does this have to be solved using only DTE? You could use MSBuild automation to do this... it has classes that can parse the contents of a csproj file.
Take a look at: Microsoft.Build.Evaluation Namespace, there is some useful information on how to load the csproj file, and then how to change it. See also the Project Class.
To solve this problem you need to add a reference path to the Project properties from DTE
To set a reference path in Visual Studio:
- In Solution Explorer, select the project.
- On the Project menu, click Properties.
- Click Reference Paths.
- In the Folder text box, specify the path of the folder that contains assemblies. To browse to the folder, click the ellipsis (…).
- Click Add Folder.
You have to do the same from the automation object
Let me know if it helps
Hard to reject a good challenge ...
I'm not there but I think I have some decent hints to move this forward.
First off I create a miniscule test addin to reproduce your problem and .. I failed!
foreach (Project project in (object[])_applicationObject.ActiveSolutionProjects)
{
((VSProject)project.Object).References.Add(@"c:\temp\test\FromFolder\bin\debug\KmlLib.dll");
}
This is a random dll from somewhere on my disk. The dll is unsigned and ended up in my csproj as (just what you want to accomplish):
<Reference Include="KmlLib">
<HintPath>..\..\FromFolder\bin\debug\KmlLib.dll</HintPath>
</Reference>
Then I noticed you dll was signed. That did not make any difference either. The next test I did was copy some standard MS dll. I picked VsWebSite.Interop.dll from \Program Files\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\ and copied it to my FromFolder\bin\debug\ Adding that as a reference suddenly reproduced your scenario> I got the include but not the hintpath.
Then the final test: I renamed VsWebSite.Interop.dll to xxVsWebSite.Interop.dll and included that dll. Suddenly the hintpath was added again!
Combining all this and your description together my guess is that when adding a reference VS first looks if the referenced dll can be found in it's current search locations (GAC, Project folder, path(?), ..) If so then no hintpath is added. If it can't be found then a hintpath is required and will be added.
To see if this theory holds up you could do two tests:
- Copy the referenced dll to a totally different folder and then reference from there --> Will still not included the hintpath because dll with same signature is still in the 'path'
- Copy and Rename the referenced dll to a totally different folder and reference the new name --> Should add the hintpath
Curious about your results :)
I had an issue recently with adding a dll reference in Visual Studio 2010, I couldn't make it add the HintPath, which caused an issue with the TFS build. I noticed that the Productivity Power Tools add-on that I installed a few weeks ago, had changed the add reference dialog. I switched off the "Searchable Add Reference Dialog" of the add-on from the Tools menu and after restart of the Visual Studio 2010 I was happy again - I was able to add the reference and this time the HintPath was there too.
精彩评论