/// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using EnvDTE;
namespace Microsoft.VisualStudio.Project
{
[CLSCompliant(false), ComVisible(true)]
public class ProjectReferenceNode : ReferenceNode
{
#region fieds
///
/// The name of the assembly this refernce represents
///
private Guid referencedProjectGuid;
private string referencedProjectName = String.Empty;
private string referencedProjectRelativePath = String.Empty;
private string referencedProjectFullPath = String.Empty;
private BuildDependency buildDependency;
///
/// This is a reference to the automation object for the referenced project.
///
private EnvDTE.Project referencedProject;
///
/// This state is controlled by the solution events.
/// The state is set to false by OnBeforeUnloadProject.
/// The state is set to true by OnBeforeCloseProject event.
///
private bool canRemoveReference = true;
///
/// Possibility for solution listener to update the state on the dangling reference.
/// It will be set in OnBeforeUnloadProject then the nopde is invalidated then it is reset to false.
///
private bool isNodeValid;
private string mAssemblyFilename = null;
#endregion
#region properties
public override string AssemblyFilename
{
get
{
if (String.IsNullOrEmpty(mAssemblyFilename))
{
mAssemblyFilename = (string)ReferencedProjectObject.Properties.Item("OutputFilename").Value;
}
return mAssemblyFilename;
}
}
public override string Url
{
get
{
return this.referencedProjectFullPath;
}
}
public override string Caption
{
get
{
return this.referencedProjectName;
}
}
internal Guid ReferencedProjectGuid
{
get
{
return this.referencedProjectGuid;
}
}
///
/// Possiblity to shortcut and set the dangling project reference icon.
/// It is ussually manipulated by solution listsneres who handle reference updates.
///
internal protected bool IsNodeValid
{
get
{
return this.isNodeValid;
}
set
{
this.isNodeValid = value;
}
}
///
/// Controls the state whether this reference can be removed or not. Think of the project unload scenario where the project reference should not be deleted.
///
internal bool CanRemoveReference
{
get
{
return this.canRemoveReference;
}
set
{
this.canRemoveReference = value;
}
}
internal string ReferencedProjectName
{
get { return this.referencedProjectName; }
}
private static EnvDTE.Project FindProject(ProjectItems projectList, string referencedPathName)
{
foreach (ProjectItem xItem in projectList)
{
var xProj = xItem.SubProject;
if (xProj == null)
{
continue;
}
// if this project is a solution folder, iterate it's child items
if (string.Compare(EnvDTE.Constants.vsProjectKindSolutionItems, xProj.Kind, StringComparison.OrdinalIgnoreCase) == 0)
{
var xResult = FindProject(xProj.ProjectItems, referencedPathName);
if (xResult != null)
{
return xResult;
}
continue;
}
//Skip this project if it is an umodeled project (unloaded)
if (string.Compare(EnvDTE.Constants.vsProjectKindUnmodeled, xProj.Kind, StringComparison.OrdinalIgnoreCase) == 0)
{
continue;
}
// Get the full path of the current project.
EnvDTE.Property pathProperty = null;
try
{
pathProperty = xProj.Properties.Item("FullPath");
if (null == pathProperty)
{
// The full path should alway be availabe, but if this is not the
// case then we have to skip it.
continue;
}
}
catch (ArgumentException)
{
continue;
}
string prjPath = pathProperty.Value.ToString();
EnvDTE.Property fileNameProperty = null;
// Get the name of the project file.
try
{
fileNameProperty = xProj.Properties.Item("FileName");
if (null == fileNameProperty)
{
// Again, this should never be the case, but we handle it anyway.
continue;
}
}
catch (ArgumentException)
{
continue;
}
prjPath = System.IO.Path.Combine(prjPath, fileNameProperty.Value.ToString());
// If the full path of this project is the same as the one of this
// reference, then we have found the right project.
if (NativeMethods.IsSamePath(prjPath, referencedPathName))
{
return xProj;
}
}
return null;
}
private static EnvDTE.Project FindProject(Projects projectList, string referencedPathName)
{
foreach (EnvDTE.Project prj in projectList)
{
// if this project is a solution folder, iterate it's child items
if (string.Compare(EnvDTE.Constants.vsProjectKindSolutionItems, prj.Kind, StringComparison.OrdinalIgnoreCase) == 0)
{
var xResult = FindProject(prj.ProjectItems, referencedPathName);
if (xResult != null)
{
return xResult;
}
continue;
}
//Skip this project if it is an umodeled project (unloaded)
if (string.Compare(EnvDTE.Constants.vsProjectKindUnmodeled, prj.Kind, StringComparison.OrdinalIgnoreCase) == 0)
{
continue;
}
// Get the full path of the current project.
EnvDTE.Property pathProperty = null;
try
{
pathProperty = prj.Properties.Item("FullPath");
if (null == pathProperty)
{
// The full path should alway be availabe, but if this is not the
// case then we have to skip it.
continue;
}
}
catch (ArgumentException)
{
continue;
}
string prjPath = pathProperty.Value.ToString();
EnvDTE.Property fileNameProperty = null;
// Get the name of the project file.
try
{
fileNameProperty = prj.Properties.Item("FileName");
if (null == fileNameProperty)
{
// Again, this should never be the case, but we handle it anyway.
continue;
}
}
catch (ArgumentException)
{
continue;
}
prjPath = System.IO.Path.Combine(prjPath, fileNameProperty.Value.ToString());
// If the full path of this project is the same as the one of this
// reference, then we have found the right project.
if (NativeMethods.IsSamePath(prjPath, referencedPathName))
{
return prj;
}
}
return null;
}
///
/// Gets the automation object for the referenced project.
///
internal EnvDTE.Project ReferencedProjectObject
{
get
{
// If the referenced project is null then re-read.
if(this.referencedProject == null)
{
// Search for the project in the collection of the projects in the
// current solution.
EnvDTE.DTE dte = (EnvDTE.DTE)this.ProjectMgr.GetService(typeof(EnvDTE.DTE));
if((null == dte) || (null == dte.Solution))
{
return null;
}
referencedProject = FindProject(dte.Solution.Projects, referencedProjectFullPath);
}
return this.referencedProject;
}
set
{
this.referencedProject = value;
}
}
///
/// Gets the full path to the assembly generated by this project.
///
internal string ReferencedProjectOutputPath
{
get
{
// Make sure that the referenced project implements the automation object.
if(null == this.ReferencedProjectObject)
{
return null;
}
// Get the configuration manager from the project.
EnvDTE.ConfigurationManager confManager = this.ReferencedProjectObject.ConfigurationManager;
if(null == confManager)
{
return null;
}
// Get the active configuration.
EnvDTE.Configuration config = confManager.ActiveConfiguration;
if(null == config)
{
return null;
}
// Get the output path for the current configuration.
EnvDTE.Property outputPathProperty = config.Properties.Item("OutputPath");
if(null == outputPathProperty)
{
return null;
}
string outputPath = outputPathProperty.Value.ToString();
// Ususally the output path is relative to the project path, but it is possible
// to set it as an absolute path. If it is not absolute, then evaluate its value
// based on the project directory.
if(!System.IO.Path.IsPathRooted(outputPath))
{
string projectDir = System.IO.Path.GetDirectoryName(referencedProjectFullPath);
outputPath = System.IO.Path.Combine(projectDir, outputPath);
}
// Now get the name of the assembly from the project.
// Some project system throw if the property does not exist. We expect an ArgumentException.
EnvDTE.Property assemblyNameProperty = null;
try
{
assemblyNameProperty = this.ReferencedProjectObject.Properties.Item("OutputFileName");
}
catch(ArgumentException)
{
}
if(null == assemblyNameProperty)
{
return null;
}
// build the full path adding the name of the assembly to the output path.
outputPath = System.IO.Path.Combine(outputPath, assemblyNameProperty.Value.ToString());
return outputPath;
}
}
private Automation.OAProjectReference projectReference;
internal override object Object
{
get
{
if(null == projectReference)
{
projectReference = new Automation.OAProjectReference(this);
}
return projectReference;
}
}
#endregion
#region ctors
///
/// Constructor for the ReferenceNode. It is called when the project is reloaded, when the project element representing the refernce exists.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
public ProjectReferenceNode(ProjectNode root, ProjectElement element)
: base(root, element)
{
this.referencedProjectRelativePath = this.ItemNode.GetMetadata(ProjectFileConstants.Include);
Debug.Assert(!String.IsNullOrEmpty(this.referencedProjectRelativePath), "Could not retrive referenced project path form project file");
string guidString = this.ItemNode.GetMetadata(ProjectFileConstants.Project);
base.ReferenceIdentifier = "project:" + guidString;
// Continue even if project setttings cannot be read.
try
{
this.referencedProjectGuid = new Guid(guidString);
this.buildDependency = new BuildDependency(this.ProjectMgr, this.referencedProjectGuid);
this.ProjectMgr.AddBuildDependency(this.buildDependency);
}
finally
{
Debug.Assert(this.referencedProjectGuid != Guid.Empty, "Could not retrive referenced project guidproject file");
this.referencedProjectName = this.ItemNode.GetMetadata(ProjectFileConstants.Name);
Debug.Assert(!String.IsNullOrEmpty(this.referencedProjectName), "Could not retrive referenced project name form project file");
}
Uri uri = new Uri(this.ProjectMgr.BaseURI.Uri, this.referencedProjectRelativePath);
if(uri != null)
{
this.referencedProjectFullPath = Microsoft.VisualStudio.Shell.Url.Unescape(uri.LocalPath, true);
}
}
///
/// constructor for the ProjectReferenceNode
///
public ProjectReferenceNode(ProjectNode root, string referencedProjectName, string projectPath, string projectReference)
: base(root)
{
Debug.Assert(root != null && !String.IsNullOrEmpty(referencedProjectName) && !String.IsNullOrEmpty(projectReference)
&& !String.IsNullOrEmpty(projectPath), "Can not add a reference because the input for adding one is invalid.");
this.referencedProjectName = referencedProjectName;
int indexOfSeparator = projectReference.IndexOf('|');
ReferenceIdentifier = "ProjectReference:" + projectReference;
string fileName = String.Empty;
// Unfortunately we cannot use the path part of the projectReference string since it is not resolving correctly relative pathes.
if(indexOfSeparator != -1)
{
string projectGuid = projectReference.Substring(0, indexOfSeparator);
base.ReferenceIdentifier = "project:" + projectGuid;
this.referencedProjectGuid = new Guid(projectGuid);
if(indexOfSeparator + 1 < projectReference.Length)
{
string remaining = projectReference.Substring(indexOfSeparator + 1);
indexOfSeparator = remaining.IndexOf('|');
if(indexOfSeparator == -1)
{
fileName = remaining;
}
else
{
fileName = remaining.Substring(0, indexOfSeparator);
}
}
}
Debug.Assert(!String.IsNullOrEmpty(fileName), "Can not add a project reference because the input for adding one is invalid.");
// Did we get just a file or a relative path?
Uri uri = new Uri(projectPath);
string referenceDir = PackageUtilities.GetPathDistance(this.ProjectMgr.BaseURI.Uri, uri);
Debug.Assert(!String.IsNullOrEmpty(referenceDir), "Can not add a project reference because the input for adding one is invalid.");
string justTheFileName = Path.GetFileName(fileName);
this.referencedProjectRelativePath = Path.Combine(referenceDir, justTheFileName);
this.referencedProjectFullPath = Path.Combine(projectPath, justTheFileName);
this.buildDependency = new BuildDependency(this.ProjectMgr, this.referencedProjectGuid);
}
#endregion
#region methods
protected override NodeProperties CreatePropertiesObject()
{
return new ProjectReferencesProperties(this);
}
///
/// The node is added to the hierarchy and then updates the build dependency list.
///
public override void AddReference()
{
if(this.ProjectMgr == null)
{
return;
}
base.AddReference();
this.ProjectMgr.AddBuildDependency(this.buildDependency);
return;
}
///
/// Overridden method. The method updates the build dependency list before removing the node from the hierarchy.
///
public override void Remove(bool removeFromStorage)
{
if(this.ProjectMgr == null || !this.CanRemoveReference)
{
return;
}
this.ProjectMgr.RemoveBuildDependency(this.buildDependency);
base.Remove(removeFromStorage);
return;
}
///
/// Links a reference node to the project file.
///
protected override void BindReferenceData()
{
Debug.Assert(!String.IsNullOrEmpty(this.referencedProjectName), "The referencedProjectName field has not been initialized");
Debug.Assert(this.referencedProjectGuid != Guid.Empty, "The referencedProjectName field has not been initialized");
this.ItemNode = new ProjectElement(this.ProjectMgr, this.referencedProjectRelativePath, ProjectFileConstants.ProjectReference);
this.ItemNode.SetMetadata(ProjectFileConstants.Name, this.referencedProjectName);
this.ItemNode.SetMetadata(ProjectFileConstants.Project, this.referencedProjectGuid.ToString("B"));
this.ItemNode.SetMetadata(ProjectFileConstants.Private, true.ToString());
}
///
/// Defines whether this node is valid node for painting the refererence icon.
///
///
protected override bool CanShowDefaultIcon()
{
if(this.referencedProjectGuid == Guid.Empty || this.ProjectMgr == null || this.ProjectMgr.IsClosed || this.isNodeValid)
{
return false;
}
IVsHierarchy hierarchy = null;
hierarchy = VsShellUtilities.GetHierarchy(this.ProjectMgr.Site, this.referencedProjectGuid);
if(hierarchy == null)
{
return false;
}
//If the Project is unloaded return false
if(this.ReferencedProjectObject == null)
{
return false;
}
return (!String.IsNullOrEmpty(this.referencedProjectFullPath) && File.Exists(this.referencedProjectFullPath));
}
///
/// Checks if a project reference can be added to the hierarchy. It calls base to see if the reference is not already there, then checks for circular references.
///
/// The error handler delegate to return
///
protected override bool CanAddReference(out CannotAddReferenceErrorMessage errorHandler)
{
// When this method is called this refererence has not yet been added to the hierarchy, only instantiated.
if(!base.CanAddReference(out errorHandler))
{
return false;
}
errorHandler = null;
if(this.IsThisProjectReferenceInCycle())
{
errorHandler = new CannotAddReferenceErrorMessage(ShowCircularReferenceErrorMessage);
return false;
}
return true;
}
private bool IsThisProjectReferenceInCycle()
{
return IsReferenceInCycle(this.referencedProjectGuid);
}
private void ShowCircularReferenceErrorMessage()
{
string message = String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.ProjectContainsCircularReferences, CultureInfo.CurrentUICulture), this.referencedProjectName);
string title = string.Empty;
OLEMSGICON icon = OLEMSGICON.OLEMSGICON_CRITICAL;
OLEMSGBUTTON buttons = OLEMSGBUTTON.OLEMSGBUTTON_OK;
OLEMSGDEFBUTTON defaultButton = OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST;
VsShellUtilities.ShowMessageBox(this.ProjectMgr.Site, title, message, icon, buttons, defaultButton);
}
///
/// Recursively search if this project reference guid is in cycle.
///
private bool IsReferenceInCycle(Guid projectGuid)
{
IVsHierarchy hierarchy = VsShellUtilities.GetHierarchy(this.ProjectMgr.Site, projectGuid);
IReferenceContainerProvider provider = hierarchy as IReferenceContainerProvider;
if(provider != null)
{
IReferenceContainer referenceContainer = provider.GetReferenceContainer();
Debug.Assert(referenceContainer != null, "Could not found the References virtual node");
foreach(ReferenceNode refNode in referenceContainer.EnumReferences())
{
ProjectReferenceNode projRefNode = refNode as ProjectReferenceNode;
if(projRefNode != null)
{
if(projRefNode.ReferencedProjectGuid == this.ProjectMgr.ProjectIDGuid)
{
return true;
}
if(this.IsReferenceInCycle(projRefNode.ReferencedProjectGuid))
{
return true;
}
}
}
}
return false;
}
#endregion
}
}