How to have Visual Studio 2017 accept modifications to csproj file
Asked Answered
B

1

16

I've developed a code generator for internal use where code assets (POCOs) are generated based off of C# interfaces. The code generation process programmatically adds/removes items to csproj file. The workflow is as follows: a developer adds a new C# interface, or removes an existing C# interface in Visual Studio 2017. If the developer saves the project file first then runs the code generator, then everything works as expected. Code-generated assets are either added to the project (or removed) and Visual Studio reflects those changes accordingly. However, if the developer fails to save the csproj file before running code generator, and has deleted a C# interface, then the code-generated assets are not being removed from the project because Visual Studio is not accepting the csproj file modifications.

Inside the code generator, I'm physically removing the references to the code-generated files that are deleted and am saving the csproj file. I verify that the referenced files are removed from the csproj file by opening the csproj up in notepad. However, as soon as I bring Visual Studio into focus, Visual Studio recognizes that the csproj file has changed and asks if I want to discard, overwrite, save as, etc and the changes made to csproj file from my code generation process are lost. Visual Studio adds the references to the deleted files back into the csproj file. I've tried discard, overwrite, save as, etc and I'm not getting Visual Studio to accept the newly modified csproj file (which has the references to deleted files removed).

Here's my code for removing code-generated assets:

using Microsoft.Build.Evaluation;
using Microsoft.Build.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

    public static void RemoveGeneratedFilesFromProject(String projectPath)
    {
        UnloadAnyProject();

        var project = ProjectCollection.GlobalProjectCollection.LoadedProjects.FirstOrDefault(pr => pr.FullPath == projectPath);

        //ATTEMPT TO SAVE PROJECT IN CASE DEVELOPER DID NOT...
        project.Save();


        //GET A LIST OF ITEMS CONTAINING PATH TO CODE-GENERATED ASSETS ("Generated\API")
        IList<ProjectItem> generatedItemsList = project.GetItems("Compile").Where(item => item.EvaluatedInclude.Contains(@"Generated\Api")).ToList();

        foreach (var item in generatedItemsList)
        {
            project.RemoveItem(item);
        }

        //SAVE PROJECT TO REFLECT ALL OF THE CODE GENERATED ITEMS REMOVED FROM PROJECT FILE
        project.Save();

        UnloadAnyProject();
    }

    private static void UnloadAnyProject()
    {
        ProjectCollection projcoll = ProjectCollection.GlobalProjectCollection;

        foreach (Project project in projcoll.LoadedProjects)
        {
            ProjectCollection mypcollection = project.ProjectCollection;
            mypcollection.UnloadProject(project);
        }
    }

Is it possible to have Visual Studio just accept the new csproj file? Is there some setting I need to make to csproj file when removing assets? Having Visual Studio balk at the modified csproj file is hindering the usefulness of the code generator for removing code-generated assets no longer needed (stemming from physically deleting a C# interface file).

EDIT

Here's a video showing the process of running the T4 generator inside Visual Studio generating C# assets based on a C# interface. I delete the source C# interface, re-run code generator and the project file is updated accordingly causing the project to be reloaded.

https://www.screencast.com/t/JWTE0LpkXZGX

The problem isn't that the project gets reloaded. The problem is the code generator updates and saves the csproj file outside of Visual Studio, which causes Visual Studio to be confused because the csproj file changed. How do you get Visual Studio to 'silently' accept the changes saved to csproj file?

Thanks for your help.

Berkshire answered 29/9, 2017 at 5:18 Comment(11)
are you running the code generator from an msbuild target during the build or outside the build process as a standalone exe / tool? most approaches to generation integrate into msbuild so they can dynamically add code even during design-time builds in VS without needing to edit the project fileLyric
So basically the first call to project.Save() has no effect in VS?Detector
It's a stand-alone code generator written with T4 files. I'm not sure how to integrate T4 with MS Build. The first project.save() does not seem to effect visual studio. The 2nd save happens after removing obsolete code assets and VS gets prompted to reload. But it seems VS has it's own version of the project in memory which is different the csproj file that was loaded/modified/saved inside scope of code generator.Berkshire
Here's a video of the code generator in action inside VS. You will see that when I delete a C# interface without saving the project, VS realizes the csproj file for DataContracts project has changed. The user is prompted to discard, save as, overwrite, etc. Selecting 'save as' does not remove the deleted asset from VS. The reference still exists in csproj file. screencast.com/t/JWTE0LpkXZGXBerkshire
@MartinUllrich - I'm generating the contents of files vie TextTransform and using System.IO.File to physically write the file to a path. This is why I have to modify csproj file manually so it becomes aware of the new file. Are you saying I can reference Microsoft.Build assembly in my T4 and use Microsoft.Build to add the newly minted file?Berkshire
Have you explored creating a Visual Studio extension instead of a stand-alone code generator?Cope
@HelderSepu thanks for the reply. I have not. I know how to generate code assets via T4. I have a lot of work wrapped up in T4. Can Visual Studio extension work with my existing code generation T4 scripts?Berkshire
As an alternative approach, is it a strict requirement that Visual Studio reload the project? Can you instead change the project type to something that is more flexible that allows you to add/remove files without reloading the entire project?Aryan
@FriendlyGuy, thanks for response. I can try your idea. The code assets generated are C# and need to be built. Not sure if changing project type affects building the project into a DLL.Berkshire
You could try to unload changed project from sln, and then after generation process add them back.Sedulity
Would something on these lines work for you? helixoft.com/blog/…. Getting the Env DTE of the current project and then modifying the solution?Sunstroke
G
5

Modifying the project file by yourself while it's loaded into Visual Studio isn't a great idea. Even if you find a way to force it to reload the project, it will still need to reload it, which is a major nuisance.

It's much better to access the EnvDTE from a T4 template, and modify the project file through that. This object gives you access to Visual Studio's project model.

Note that the user will still need to save the modified project file by default, as it will be seen as dirty by VS, but this behavior is consistent with every other project file modification you can do through VS. You can force VS to save the project if you really need it, though.

Here's what you need to do to access VS, as documented here:

Set the hostspecific attribute to true:

<#@ template debug="false" hostspecific="true" language="C#" #>

Import EnvDTE:

<#@ assembly name="EnvDTE" #>
<#@ import namespace="EnvDTE" #>

Get the dte object:

<#
    var dte = (DTE)((IServiceProvider)Host).GetService(typeof(DTE));
#>

And now you have full access to VS's project APIs. Beware that with this approach you lose the ability to execute the template outside of Visual Studio.


Here's a full example of how to add a file beside your template:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="EnvDTE" #>
<#@ output extension=".txt" #>
<#
    // Get the DTE
    var dte = (DTE)((IServiceProvider)Host).GetService(typeof(DTE));

    // Find the currently running template file in the project structure
    var template = dte.Solution.FindProjectItem(Host.TemplateFile);

    // Write something to a dummy file next to the template
    var filePath = System.IO.Path.ChangeExtension(Host.TemplateFile, "foo");
    System.IO.File.WriteAllText(filePath, "Hello, world!");

    // Add the file as a subitem of the template
    var fileItem = dte.Solution.FindProjectItem(filePath);
    if (fileItem == null)
    {
        template.ProjectItems.AddFromFile(filePath);

        // If you really want to, you can force VS to save the project,
        // though I wouldn't recommend this
        template.ContainingProject.Save();
    }
#>

Here's the result in the solution explorer:

result

Growler answered 16/10, 2017 at 18:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.