/// 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 } }