/* **************************************************************************** * * Copyright (c) Microsoft Corporation. * * This source code is subject to terms and conditions of the Apache License, Version 2.0. A * copy of the license can be found in the License.html file at the root of this distribution. If * you cannot locate the Apache License, Version 2.0, please send an email to * vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound * by the terms of the Apache License, Version 2.0. * * You must not remove this notice, or any other, from this software. * * ***************************************************************************/ using System; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio; namespace Microsoft.VisualStudio.Project { public class ProjectReferenceNode : ReferenceNode { #region fields /// /// 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; private FileSystemWatcher fileWatcher; public bool IgnoreFileChanges = false; #endregion #region properties public override string Url { get { return this.referencedProjectFullPath; } } public virtual string RelativeUrl { get { return this.referencedProjectRelativePath; } } public override string Caption { get { return this.referencedProjectName; } } public 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. /// public bool IsValid { get { return this.ReferencedProjectObject != null; } } /// /// 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. /// public bool CanRemoveReference { get { return this.canRemoveReference; } set { this.canRemoveReference = value; } } public string ReferencedProjectName { get { return this.referencedProjectName; } } /// /// Gets the automation object for the referenced project. /// public EnvDTE.Project ReferencedProjectObject { get { // If the referenced project is null then re-read. if (this.referencedProject == null) { this.referencedProject = VsUtilities.GetProject(this.ProjectMgr.Site, referencedProjectFullPath); } return this.referencedProject; } set { this.referencedProject = value; } } public string UniqueName { get { return this.ReferencedProjectGuid.ToString("B") + "|" + CommonUtils.TrimUpPaths(this.referencedProjectRelativePath); } } /// /// Gets the full path to the assembly generated by this project. /// public string ReferencedProjectOutputPath { get { // Make sure that the referenced project implements the automation object. if (null == this.ReferencedProjectObject) { return null; } EnvDTE.ConfigurationManager confManager = null; // Get the configuration manager from the project. try { confManager = this.ReferencedProjectObject.ConfigurationManager; } catch (COMException) { //The project could be not available. } if (null == confManager) { return null; } // Get the active configuration. EnvDTE.Configuration config = confManager.ActiveConfiguration; if (null == config) { return null; } if (null == config.Properties) { return null; } // Get the output path for the current configuration. EnvDTE.Property outputPathProperty = config.Properties.Item("OutputPath"); if (null == outputPathProperty || outputPathProperty.Value == null) { return null; } // Usually the output path is relative to the project path. If it is set as an // absolute path, this call has no effect. string outputPath = CommonUtils.GetAbsoluteDirectoryPath( Path.GetDirectoryName(referencedProjectFullPath), outputPathProperty.Value.ToString()); // 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 = Path.Combine(outputPath, assemblyNameProperty.Value.ToString()); return outputPath; } } public string AssemblyName { get { // 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; if (ReferencedProjectObject != null && !(ReferencedProjectObject is Automation.OAProject)) // our own projects don't have assembly names { try { assemblyNameProperty = this.ReferencedProjectObject.Properties.Item(ProjectFileConstants.AssemblyName); } catch (ArgumentException) { } if (assemblyNameProperty != null) { return assemblyNameProperty.Value.ToString(); } } return null; } } private Automation.OAProjectReference projectReference; public 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 retrieve 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"); } try { this.referencedProjectFullPath = CommonUtils.GetAbsoluteFilePath(this.ProjectMgr.ProjectHome, this.referencedProjectRelativePath); } catch { } //Just make an invalid reference. } /// /// constructor for the ProjectReferenceNode /// public ProjectReferenceNode(ProjectNode root, string referencedProjectName, string projectPath, string projectReference) : base(root) { 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); } } } string justTheFileName = Path.GetFileName(fileName); try { this.referencedProjectFullPath = CommonUtils.GetAbsoluteFilePath(Path.GetDirectoryName(projectPath), justTheFileName); // TODO: Maybe referenced projects should be relative to ProjectDir? this.referencedProjectRelativePath = CommonUtils.GetRelativeFilePath(this.ProjectMgr.ProjectHome, this.referencedProjectFullPath); } catch { } //Make an invalid reference. this.buildDependency = new BuildDependency(this.ProjectMgr, this.referencedProjectGuid); this.ProjectMgr.AddBuildDependency(this.buildDependency); } #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 MsBuildProjectElement(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.IsValid) { 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) { // TODO: This has got to be wrong, it doesn't work w/ other project types. IVsHierarchy hierarchy = VsShellUtilities.GetHierarchy(this.ProjectMgr.Site, projectGuid); IReferenceContainerProvider provider = hierarchy.GetProject().GetCommonProject() 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 protected override void ResolveReference() { string oldOutputPath = this.ReferencedProjectOutputPath; base.ResolveReference(); string outputPath = this.ReferencedProjectOutputPath; if (!string.IsNullOrEmpty(outputPath) && (oldOutputPath != outputPath || fileWatcher == null)) { if (fileWatcher != null) fileWatcher.Dispose(); fileWatcher = new FileSystemWatcher(Path.GetDirectoryName(outputPath)); fileWatcher.Changed += fileWatcher_Changed; fileWatcher.EnableRaisingEvents = true; } } void fileWatcher_Changed(object sender, FileSystemEventArgs e) { if (e.ChangeType == WatcherChangeTypes.Changed && IgnoreFileChanges == false && e.FullPath == this.ReferencedProjectOutputPath) { System.Threading.Tasks.Task.Run(() => { this.RefreshReference(true); }); } } protected override void Dispose(bool disposing) { if (fileWatcher != null) { fileWatcher.Dispose(); } base.Dispose(disposing); } #if DEV11_OR_LATER public override bool Equals(IVsReference other) { if (other is IVsProjectReference) { IVsProjectReference projectReference = (IVsProjectReference)other; return projectReference.Name == this.Name && projectReference.FullPath == this.FullPath && projectReference.Identity == this.ReferencedProjectGuid.ToString("B") && projectReference.ReferenceSpecification == this.UniqueName; } return false; } #endif public override string ReferenceType { get { return ProjectFileConstants.ProjectReference; } } } }