From 83139d1f81316802db817381b7ef59a42cf01e2a Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Wed, 12 Aug 2026 15:25:09 +0200 Subject: [PATCH] [Vsintegration] Show project references of SDK style projects in the Dependencies node The "Projects" node under "Dependencies" stayed empty when a solution with SDK style X# projects was opened. It only appeared after adding a project reference interactively, and was gone again after the next reload. No ProjectReferenceNode was created at all: - XSharpSdkProjectNode.Clean() strips the Project (guid) and Name metadata from every ProjectReference item before saving, so a saved .xsproj only contains . - XSharpReferenceContainerNode.CreateProjectReferenceNode() resolved guid and name for the SDK case but never wrote them to the project element. - The ProjectReferenceNode constructor deliberately skips the "invent a guid" fallback for SDK projects and then called new Guid(guidString) on an empty string inside a try/finally without a catch, so it threw FormatException. - ReferenceContainerNode.CreateReferenceNode() swallows that exception without logging and returns null, so the node silently disappeared. Because the "Projects" folder is only created when the first XSharpProjectReferenceNode is added, the whole folder was missing. - Adding a reference through the dialog uses the other constructor, which parses the guid from the bstrProjRef string, which is why that path worked. FixReferences(), which is supposed to complete these references once the solution has finished loading, had nothing left to iterate over. It was broken on its own as well: when the referenced project was found it never assigned refnodeGuid, so it always reported failure. - XSharpReferenceContainerNode: store the resolved guid and name on the in memory project element and resolve foreign projects via ProjectInfo / GetProjectGuid, the same way FixReferences() does. Clean() still keeps them out of the saved project file. Node creation is wrapped in try/catch with Logger.Exception so a failure can no longer drop a reference silently. - ProjectReferenceNode: tolerate a missing or malformed guid. The node is created with Guid.Empty and without a build dependency instead of throwing. Added UpdateReferencedProjectGuid() to attach the guid and the build dependency later and redraw the icon. - XSharpProjectNode.FixReferences(): use refnode.ProjectIDGuid when the referenced project is one of ours, resolve per node instead of skipping the remaining nodes after the first failure, push the guid into the node and log what stayed unresolved. - ProjectNode.BuildDependencies: do not register a ProjectInfo with an empty guid. It would be cached by url and would block the later resolution. - XSharpShellEvents: also complete incomplete references on OnAfterBackgroundSolutionLoadComplete. With deferred solution load not all projects exist yet when OnAfterOpenSolution fires. Co-Authored-By: Claude Opus 5 --- src/VisualStudio/ProjectBase/ProjectNode.cs | 8 +++ .../ProjectBase/ProjectReferenceNode.cs | 55 +++++++++++++++---- .../ProjectPackage/Nodes/XSharpProjectNode.cs | 21 ++++--- .../Nodes/XSharpReferenceContainerNode.cs | 39 ++++++++++++- .../ProjectPackage/XSharpShellEvents.cs | 17 +++++- 5 files changed, 115 insertions(+), 25 deletions(-) diff --git a/src/VisualStudio/ProjectBase/ProjectNode.cs b/src/VisualStudio/ProjectBase/ProjectNode.cs index 4467c3b8d0..79df53994c 100644 --- a/src/VisualStudio/ProjectBase/ProjectNode.cs +++ b/src/VisualStudio/ProjectBase/ProjectNode.cs @@ -6701,6 +6701,14 @@ public virtual IVsBuildDependency[] BuildDependencies var projectInfo = ProjectInfo.GetProjectInfo(url); if (projectInfo == null) { + if (node.ReferencedProjectGuid == Guid.Empty) + { + // The referenced project has not been loaded, so we do not know its guid yet. + // Do not register a ProjectInfo with an empty guid: that would be cached by url + // and would prevent the guid from being resolved later on. + Logger.Information($"No BuildDependency for project {this.Caption} on {url}: the guid of the referenced project is unknown"); + continue; + } projectInfo = new ProjectInfo(node.ReferencedProjectGuid, url); } var dependency = new BuildDependency(this, projectInfo.Id); diff --git a/src/VisualStudio/ProjectBase/ProjectReferenceNode.cs b/src/VisualStudio/ProjectBase/ProjectReferenceNode.cs index 803a943a0d..20c60bea25 100644 --- a/src/VisualStudio/ProjectBase/ProjectReferenceNode.cs +++ b/src/VisualStudio/ProjectBase/ProjectReferenceNode.cs @@ -252,6 +252,36 @@ public void DropReferencedProjectCache() referencedProjectIsCached = false; } + /// + /// Assign the guid of the referenced project. + /// + /// + /// SDK style project files do not store the guid of the referenced project, so it can only be + /// resolved when the referenced project has been loaded. This also updates the build dependency, + /// which could not be created when the node was constructed without a guid. + /// + public void UpdateReferencedProjectGuid(Guid guid) + { + if (guid == this.referencedProjectGuid) + { + return; + } + ThreadHelper.ThrowIfNotOnUIThread(); + if (this.buildDependency != null) + { + this.ProjectMgr.RemoveBuildDependency(this.buildDependency); + this.buildDependency = null; + } + this.referencedProjectGuid = guid; + if (guid != Guid.Empty) + { + this.buildDependency = new BuildDependency(this.ProjectMgr, guid); + this.ProjectMgr.AddBuildDependency(this.buildDependency); + } + this.DropReferencedProjectCache(); + this.ReDraw(UIHierarchyElement.Icon); + } + EnvDTE.Property GetPropertySave(string name, bool onConfig) { @@ -399,21 +429,26 @@ public ProjectReferenceNode(ProjectNode root, ProjectElement element) ReferencedProjectName = Path.GetFileNameWithoutExtension(referencedProjectRelativePath); } - // Continue even if project settings cannot be read. - try + // An SDK style project file does not store the guid of the referenced project, so we may + // not have one here, for example when the referenced project has not been loaded yet. + // Do not throw in that case: the node must appear in the hierarchy anyway. The guid is + // filled in later, see XSharpProjectNode.FixReferences() and UpdateReferencedProjectGuid(). + if (!Guid.TryParse(guidString, out var guid)) + { + guid = Guid.Empty; + } + this.referencedProjectGuid = guid; + if (guid != Guid.Empty) { - this.referencedProjectGuid = new Guid(guidString); - this.buildDependency = new BuildDependency(this.ProjectMgr, this.referencedProjectGuid); this.ProjectMgr.AddBuildDependency(this.buildDependency); } - finally + var name = this.ItemNode.GetMetadata(ProjectFileConstants.Name); + if (!string.IsNullOrEmpty(name)) { - //Debug.Assert(this.referencedProjectGuid != Guid.Empty, "Could not retrieve referenced project guidproject file"); - - this.ReferencedProjectName = this.ItemNode.GetMetadata(ProjectFileConstants.Name); - - //Debug.Assert(!String.IsNullOrEmpty(this.referencedProjectName), "Could not retrieve referenced project name form project file"); + // Do not clear the name that was determined above when the metadata could not be written, + // which happens for imported items. + this.ReferencedProjectName = name; } Uri uri = new Uri(this.ProjectMgr.BaseURI.Uri, this.referencedProjectRelativePath); diff --git a/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs b/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs index bc3de027d9..7ade0d6c35 100644 --- a/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs +++ b/src/VisualStudio/ProjectPackage/Nodes/XSharpProjectNode.cs @@ -1490,16 +1490,19 @@ internal bool FixReferences() var refnode = FindProject(completePath); Guid refnodeGuid = Guid.Empty; string refnodeName = child.Caption; - if (refnode == null) + if (refnode != null) + { + refnodeGuid = refnode.ProjectIDGuid; + } + else { // this must be a foreign project reference var projectInfo = ProjectInfo.GetProjectInfo(completePath); if (projectInfo == null) { - if (this.GetProjectGuid(completePath, out refnodeGuid)) + if (this.GetProjectGuid(completePath, out refnodeGuid) && refnodeGuid != Guid.Empty) { projectInfo = new ProjectInfo(refnodeGuid, completePath); - element.SetMetadata(ProjectFileConstants.Project, refnodeGuid.ToString("B").ToUpperInvariant()); } } else @@ -1511,19 +1514,19 @@ internal bool FixReferences() { element.SetMetadata(ProjectFileConstants.Project, refnodeGuid.ToString("B").ToUpperInvariant()); element.SetMetadata(ProjectFileConstants.Name, refnodeName); + sdkref.SaveProperties(); + // The node was created before the referenced project was available, so it has no + // guid and no build dependency yet. + sdkref.UpdateReferencedProjectGuid(refnodeGuid); } else { + Logger.Information($"Could not determine the guid of project {completePath}, referenced by {this.Caption}"); found = false; } - if (found) - { - sdkref.SaveProperties(); - } } - - HasIncompleteReferences = !found; } + HasIncompleteReferences = !found; this.SetProjectFileDirty(false); return found; } diff --git a/src/VisualStudio/ProjectPackage/Nodes/XSharpReferenceContainerNode.cs b/src/VisualStudio/ProjectPackage/Nodes/XSharpReferenceContainerNode.cs index 817266d169..575aa290b8 100644 --- a/src/VisualStudio/ProjectPackage/Nodes/XSharpReferenceContainerNode.cs +++ b/src/VisualStudio/ProjectPackage/Nodes/XSharpReferenceContainerNode.cs @@ -39,17 +39,40 @@ protected override ProjectReferenceNode CreateProjectReferenceNode(ProjectElemen bool changed = false; if (parent.IsSdkProject) { - // already handled in the DependenciesContainer + // An SDK style project file does not store the guid and the name of the referenced project, + // so we resolve them here and store them in the (in memory) project element. The base + // ProjectReferenceNode reads them from there. BeforeSave() removes them again, so they are + // not written to the project file, see XSharpSdkProjectNode.Clean(). name = System.IO.Path.GetFileNameWithoutExtension(path); + guid = null; if (refnode != null) { guid = refnode.ProjectIDGuid.ToString("B"); } else { - guid = null; + // Not one of our projects, or a project that has not been loaded yet + var projectInfo = ProjectInfo.GetProjectInfo(path); + if (projectInfo == null && parent.GetProjectGuid(path, out var foreignGuid) && foreignGuid != Guid.Empty) + { + projectInfo = new ProjectInfo(foreignGuid, path); + } + if (projectInfo != null && projectInfo.Id != Guid.Empty) + { + guid = projectInfo.Id.ToString("B"); + } + } + element.SetMetadata(ProjectFileConstants.Name, name); + if (string.IsNullOrEmpty(guid)) + { + // The referenced project is not available yet. The node is created without a guid and + // FixReferences() completes it when the solution has finished loading. parent.HasIncompleteReferences = true; } + else + { + element.SetMetadata(ProjectFileConstants.Project, guid); + } } else if (string.IsNullOrEmpty(guid) || string.IsNullOrEmpty(name)) { @@ -84,7 +107,17 @@ protected override ProjectReferenceNode CreateProjectReferenceNode(ProjectElemen parent.BuildProject.Save(); } - var node = (XSharpProjectReferenceNode) Activator.CreateInstance(ProjectReferenceType,this.ProjectMgr, element); + XSharpProjectReferenceNode node = null; + try + { + node = (XSharpProjectReferenceNode)Activator.CreateInstance(ProjectReferenceType, this.ProjectMgr, element); + } + catch (Exception e) + { + // Do not let this bubble up: ReferenceContainerNode.CreateReferenceNode() swallows the + // exception without logging it and the reference would disappear from the hierarchy. + Logger.Exception(e, "CreateProjectReferenceNode"); + } ReferenceNode existing = null; if (isDuplicateNode(node, ref existing)) { diff --git a/src/VisualStudio/ProjectPackage/XSharpShellEvents.cs b/src/VisualStudio/ProjectPackage/XSharpShellEvents.cs index 5783cfcd53..112d7cd99e 100644 --- a/src/VisualStudio/ProjectPackage/XSharpShellEvents.cs +++ b/src/VisualStudio/ProjectPackage/XSharpShellEvents.cs @@ -86,17 +86,23 @@ internal void Initialize() #if DEV17 private void SolutionEvents_OnAfterOpenSolution(Solution solution) { + FixIncompleteReferences(); + } + /// + /// Complete the project references of SDK style projects that could not be resolved while the + /// project was loading, because the referenced project was not loaded yet. + /// + private void FixIncompleteReferences() + { foreach (var project in XSharpProjectNode.AllProjects) { if (project.HasIncompleteReferences) { - project.FixReferences(); } } - - } + } #endif private void SolutionEvents_OnBeforeCloseSolution() { @@ -129,6 +135,11 @@ private void SolutionEvents_OnBeforeCloseSolution() private void SolutionEvents_OnAfterBackgroundSolutionLoadComplete() { ThreadHelper.ThrowIfNotOnUIThread(); +#if DEV17 + // Projects may have been loaded after OnAfterOpenSolution, so check again now that all + // projects of the solution are available. + FixIncompleteReferences(); +#endif RestoreDesignerWindows(); RestoreStartupProject(); }