Community.VisualStudio.Toolkit icon indicating copy to clipboard operation
Community.VisualStudio.Toolkit copied to clipboard

Adding Nested Files is clunky and unintuitive. - PhysicalFile.AddNestedFileFromAsync

Open RFBomb opened this issue 2 years ago • 4 comments

I'm working on an extension that generated multiple files as a CustomTool. As such, I want to add these under the associated file that causes the custom tool to run.

Unfortunately, I am unable to located the nice succinct piece of code that did this on some StackOverflow thread I saw, so I wrote the following method based on the code that this toolkit provides.

/// <summary>Adds a file to the project nested within another file in the solution explorer </summary>
        /// <param name="ParentItem"></param>
        /// <param name="ItemToAdd"></param>
        /// <returns></returns>
        public static async System.Threading.Tasks.Task AddFileToProjectAsync(FileInfo ParentItem, FileInfo ItemToAdd)
        {
            Project project = await VS.Solutions.GetActiveProjectAsync();
            PhysicalFile solutionItem = await PhysicalFile.FromFileAsync(ParentItem.FullName);
            await project.AddExistingFilesAsync(ItemToAdd.FullName);
            PhysicalFile newItem = await PhysicalFile.FromFileAsync(ItemToAdd.FullName);
            if (newItem != null) await solutionItem.AddNestedFileAsync(newItem);
        }

Looking at PhysicalFile.AddNestedFileAsync() method, all it does is attempt to update the file with the 'dependent' attribute. This functionality is fine, but the problem is that this assumes the file is already part of the project.

During my testing, this causes the file to be first added to the project, then it redoes the nesting by adding the dependency. this causes the new file to appear outside of the nest. Once VS is restarted, then it appears properly nested, but until that restart occurs the file appears to not be nested.

RFBomb avatar Oct 26 '21 18:10 RFBomb

Possible duplicate of #165. Is the project you're trying to add the nested file to a legacy-style of project, or is it the newer SDK-style project?

reduckted avatar Oct 27 '21 21:10 reduckted

I am using Visual Studio 2019. The project is a VSIX project, writing to Framework 4.7.2 and outputting to a class DLL. My source code is located here: https://github.com/RFBomb/XSDexeCustomTool

For quick reference, here are the first few lines of the .csproj file:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

Edit: After looking at #165, I agree that this is an identical issue, as we are both using the same version (15)

RFBomb avatar Oct 28 '21 19:10 RFBomb

For now, I'm reverting to using DTE. Here is my working code:

EnvDTE.DTE dte = (EnvDTE.DTE)(Package.GetGlobalService(typeof(EnvDTE.DTE)));
var rootDocumentItem = dte.Solution.FindProjectItem(ParentItem.FullName);
rootDocumentItem.ProjectItems.AddFromFile(ItemToAdd.FullName);

As far as this issue is concerned, I see several problems.

  • AddNestedFileAsync only allows you to nest a file that already exists in the project. Meaning you must first add the file to the project THEN call the nesting method. This is because it only accepts a PhysicalFile as an input parameter, which if the file doesn't already exists in the solution will be null. I recommend adding an overload that accepts a string parameter that will allow people to put a path into the method and will add the file and nest it properly, which is what the dte code I use above seems to do.

  • Nesting the file using this method will not show as nested until the project is reloaded. Maybe reloading can be silently triggered/forced after the dependency is changed, I'm not sure what ramifications that will have. if this is performed though. (theoretically none, because the only change should be the dependency attribute). Maybe add a way to trigger the refresh. This means that instead of forcing a refresh whenever each file is added, several can be nested then the refresh occurs.

As far as legacy support goes, the only thing I could come up with was parsing the CSProj xml file for the ToolsVersion attribute and reacting to that. But since I'm just going to rely on the dte code, I've commented all that out of my project.

Here is what I came up with.

Project project = await VS.Solutions.GetActiveProjectAsync();
System.Xml.XmlDocument projectXml = new System.Xml.XmlDocument();
projectXml.Load(project.FullPath);
string ProjectVer = projectXml.SelectSingleNode("//@ToolsVersion")?.Value;
projectXml = null;

if (Convert.ToSingle(ProjectVer) <= 15.0) {  /* Do Stuff */ }

RFBomb avatar Oct 28 '21 20:10 RFBomb

I agree that an overload taking a string would fix it. If it requires a reopening of the solution to show, then we're using the wrong APIs behind the scene and should fix it.

madskristensen avatar Oct 28 '21 21:10 madskristensen