/*************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. This code is licensed under the Visual Studio SDK license terms. THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. ***************************************************************************/ using System; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; 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; #endregion #region properties 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; } } /// /// 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; } this.referencedProject = this.FindReferencedProject(dte.Solution.Projects); } 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); // 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."); if (projectReference == null) { throw new ArgumentNullException("projectReference"); } this.referencedProjectName = referencedProjectName; int indexOfSeparator = projectReference.IndexOf('|'); 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); 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); } /// /// Checks whether a reference added to a given project would introduce a circular dependency. /// private bool IsReferenceInCycle(Guid projectGuid) { IVsHierarchy referencedHierarchy = VsShellUtilities.GetHierarchy(this.ProjectMgr.Site, projectGuid); var solutionBuildManager = this.ProjectMgr.Site.GetService(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager2; if (solutionBuildManager == null) { throw new InvalidOperationException("Cannot find the IVsSolutionBuildManager2 service."); } int circular; Marshal.ThrowExceptionForHR(solutionBuildManager.CalculateProjectDependencies()); Marshal.ThrowExceptionForHR(solutionBuildManager.QueryProjectDependency(referencedHierarchy, this.ProjectMgr, out circular)); return circular != 0; } private EnvDTE.Project FindReferencedProject(System.Collections.IEnumerable projects) { EnvDTE.Project refProject = null; // Search for the project in the collection of the projects in the current solution. foreach (EnvDTE.Project prj in projects) { //Skip this project if it is an umodeled project (unloaded) if (string.Compare(EnvDTE.Constants.vsProjectKindUnmodeled, prj.Kind, StringComparison.OrdinalIgnoreCase) == 0) { continue; } // Recursively iterate solution folder for the project. if (string.Compare(EnvDTE.Constants.vsProjectKindSolutionItems, prj.Kind, StringComparison.OrdinalIgnoreCase) == 0) { var containedProjects = GetContainerProjects(prj); refProject = FindReferencedProject(containedProjects); if (refProject != null) return refProject; } // Get the full path of the current project. EnvDTE.Property pathProperty = null; try { if (prj.Properties == null) { continue; } 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, referencedProjectFullPath)) { refProject = prj; break; } } return refProject; } private static System.Collections.IEnumerable GetContainerProjects(EnvDTE.Project prj) { foreach (var obj in prj.ProjectItems) { var pi = obj as EnvDTE.ProjectItem; if (pi == null) { continue; } var nestedPrj = pi.Object as EnvDTE.Project; if (nestedPrj != null) { yield return nestedPrj; } } } #endregion } }