10

Is there a way to include a file in project from command line in vs2012 ?

The reason why I'm asking is because is very frustrating to include any new file I add to the project folder whenever I use some other IDE ( like ST3 ) or when I save a file from Photoshop, etc.

I'm using Grunt for doing a lot of minifying, concatenation, running ngmin on my angular scripts, etc. There's a grunt-shell plugin that allows grunt tasks to run shell commands ( I'm already using it for unlocking locked files by TFS ). So I was thinking that I could create a task that would do the include in project for me for any new file I add ( by watching a certain folder with grunt-watch ).

Roland
  • 9,321
  • 17
  • 79
  • 135
  • What are you looking for that the two answers below don't offer? – Mark Rucker Aug 01 '13 at 14:14
  • @MarkRucker ~ some more options, I prefer the solution with C over the one with replacing the end of the csproj, because most projects I have worked on have all kinds of things in there and the last ones aren't the item groups. But I think the C one is ok, I just need some more description from the author. – Roland Aug 01 '13 at 14:31
  • OK, I'll give drphrozen a day to add detail since his answer is 90% of the way there. If he doesn't respond I'll try to answer with more detail (assuming somebody else doesn't jump in). – Mark Rucker Aug 01 '13 at 21:24

5 Answers5

9

Here is a solution using PowerShell. It is a little long, but I promise it works. I tested quite a bit.

First, the easy part. Here's how you run the script from the command prompt.

powershell -File C:\AddExistingItem.ps1 -solutionPath "C:\Test.sln" -projectName "TestAddItem" -item "C:\Test.txt"

Now the scary part, AddExistingItem.ps1:

param([String]$solutionPath, [String]$projectName, [String]$item)

#BEGIN: section can be removed if executing from within a PowerShell window
$source = @" 

namespace EnvDteUtils
{ 
    using System; 
    using System.Runtime.InteropServices; 

    public class MessageFilter : IOleMessageFilter 
    { 
        // 
        // Class containing the IOleMessageFilter 
        // thread error-handling functions. 

        // Start the filter. 
        public static void Register() 
        { 
            IOleMessageFilter newFilter = new MessageFilter();  
            IOleMessageFilter oldFilter = null;  
            CoRegisterMessageFilter(newFilter, out oldFilter); 
        } 

        // Done with the filter, close it. 
        public static void Revoke() 
        { 
            IOleMessageFilter oldFilter = null;  
            CoRegisterMessageFilter(null, out oldFilter); 
        } 

        // 
        // IOleMessageFilter functions. 
        // Handle incoming thread requests. 
        int IOleMessageFilter.HandleInComingCall(int dwCallType,  
          System.IntPtr hTaskCaller, int dwTickCount, System.IntPtr  
          lpInterfaceInfo)  
        { 
            //Return the flag SERVERCALL_ISHANDLED. 
            return 0; 
        } 

        // Thread call was rejected, so try again. 
        int IOleMessageFilter.RetryRejectedCall(System.IntPtr  
          hTaskCallee, int dwTickCount, int dwRejectType) 
        { 
            if (dwRejectType == 2) 
            // flag = SERVERCALL_RETRYLATER. 
            { 
                // Retry the thread call immediately if return >=0 &  
                // <100. 
                return 99; 
            } 
            // Too busy; cancel call. 
            return -1; 
        } 

        int IOleMessageFilter.MessagePending(System.IntPtr hTaskCallee,  
          int dwTickCount, int dwPendingType) 
        { 
            //Return the flag PENDINGMSG_WAITDEFPROCESS. 
            return 2;  
        } 

        // Implement the IOleMessageFilter interface. 
        [DllImport("Ole32.dll")] 
        private static extern int  
          CoRegisterMessageFilter(IOleMessageFilter newFilter, out  
          IOleMessageFilter oldFilter); 
    } 

    [ComImport(), Guid("00000016-0000-0000-C000-000000000046"),  
    InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] 
    interface IOleMessageFilter  
    { 
        [PreserveSig] 
        int HandleInComingCall(  
            int dwCallType,  
            IntPtr hTaskCaller,  
            int dwTickCount,  
            IntPtr lpInterfaceInfo); 

        [PreserveSig] 
        int RetryRejectedCall(  
            IntPtr hTaskCallee,  
            int dwTickCount, 
            int dwRejectType); 

        [PreserveSig] 
        int MessagePending(  
            IntPtr hTaskCallee,  
            int dwTickCount, 
            int dwPendingType); 
    } 
} 
"@ 

Add-Type -TypeDefinition $source      

[EnvDTEUtils.MessageFilter]::Register()
#END: section can be removed if executing from within a PowerShell window

$IDE = New-Object -ComObject VisualStudio.DTE

$IDE.Solution.Open("$solutionPath")

$project = $IDE.Solution.Projects | ? { $_.Name -eq "$projectName" }
$project.ProjectItems.AddFromFile("$item") | Out-Null
$project.Save()

$IDE.Quit()

#BEGIN: section can be removed if executing from within a PowerShell window
[EnvDTEUtils.MessageFilter]::Revoke()
#END: section can be removed if executing from within a PowerShell window

95% of the code is only there to let you run from the command prompt. If you are writing and running code directly in PowerShell you can leave it out and go straight to $IDE = New-Object -ComObject VisualStudio.DTE.

Here is a blog post explaining why that scary stuff is needed.
And here is another one on the same thing but in C#.

Another thing worth noting. I tried using the EnvDTE assemblies, like you would in .net, but I kept getting a COM registration error. Once I started using the COM objects directly everything worked. I don't know enough about COM to really venture a guess as to why this is.

EDIT

If you receive this error when trying to run the script from the command prompt:

Execution Policy Error

Then you need to run this first (you should only need to run it once ever.)

powershell -command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned"

Here is a good, in-depth explanation of what that command is doing.

Mark Rucker
  • 6,952
  • 4
  • 39
  • 65
  • I get an error if running from CP as admin: `File .... cannot be loaded because running scripts is disabled on this system. ...` – Roland Aug 05 '13 at 06:24
  • oh ok, yeah it is probably your execution policy. Let me do some tests and I'll get back with you shortly – Mark Rucker Aug 05 '13 at 13:32
  • I'll give it a shot now, just that I hope it will work when I'll run this thing from a node process using Grunt (some plugin for grunt called [grunt-shell](https://github.com/sindresorhus/grunt-shell)), because I need to somehow watch the files that I add or remove and process that according to the action I do ... – Roland Aug 05 '13 at 15:27
  • I just executed the command and this is what I've got: [Screenshot](https://dl.dropboxusercontent.com/u/100425299/test.png) – Roland Aug 05 '13 at 15:34
  • @rolandjitsu It looks like your project name doesn't match so it can't find the project. (you put an e in the command line on mobil). While the comparison isn't case sensitive it is strict so the names have to match exactly. Here's a modified version of the script that takes a solution and returns all valid project names from it if it would help (http://cl.ly/code/0I1u2m3U141N). – Mark Rucker Aug 05 '13 at 16:42
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/34855/discussion-between-mark-rucker-and-rolandjitsu) – Mark Rucker Aug 05 '13 at 16:44
  • I think it would be fair to say you get the bounty as your solution got the closest to what I need. And it does work, I haven't noticed the project name was wrong ... I would though have a question, would it be possible to somehow integrate with @Victor's solution. I like the idea of using the nodejs module as middleware as I'm already using nodejs and grunt ? Maybe have this powershell command as `.dll` assembly ? Because the issue that @Victor's solution had is that the libs that he is requiring aren't found in the project. – Roland Aug 05 '13 at 17:01
  • Thanks, yeah I'm not sure how edge handles dll references. It sounds to me like edge doesn't have a reference to EnvDTE.dll so it doesn't know what the using statements belong to. As for turning my script into a dll I don't think you can do that since it isn't a formal .NET language. You'd need something to turn PS-script into Intermediate Language, and I haven't heard of anything that does that. – Mark Rucker Aug 05 '13 at 17:29
5

You could just create a small exe to handle this for you:

using System.IO;
using Microsoft.Build.Evaluation;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;

namespace AddTfsItem
{
  class Program
  {
    static void Main(string[] args)
    {
      var projectFile = args[0];
      var projectDirectory = Path.GetDirectoryName(projectFile);
      var sourceFile = args[1];
      var itemType = args.Length > 2 ? args[2] : "Compile";

      var workspaceInfo = Workstation.Current.GetLocalWorkspaceInfo(projectFile);
      using (var server = new TfsTeamProjectCollection(workspaceInfo.ServerUri))
      {
        var workspace = workspaceInfo.GetWorkspace(server);

        workspace.PendEdit(projectFile);

        using (var projectCollection = new ProjectCollection())
        {
            var project = new Project(projectFile, null, null, projectCollection);
            project.AddItem(itemType, sourceFile);
            project.Save();
        }

        workspace.PendAdd(Path.Combine(projectDirectory, sourceFile));
      }
    }
  }
}

Structure of C:\Projects\Test:

  • Test.csproj
  • SomeFolder
    • Tester.cs

Usage: AddTfsItem.exe C:\Projects\Test\Test.csproj SomeFolder\Tester.cs

Notice that the file has to be relative to the .csproj file.

drphrozen
  • 317
  • 3
  • 4
  • Could you explain what does this cs do ? From what I see I run this cs exe while feeding it with a path to the csproj file and the file I'm including ? I also have to keep this exe inside the project ? Or is it the file I'm including that has to be relative to the proj ? – Roland Jul 29 '13 at 15:07
  • The cs file uses the Build and TFS api's (usually installed with VS) to add the file in the csproj file and the tfs. Correct, hence the "Usage" :) Or you could dump the exe in a %path% directory. Everything revolves around the csproj file, so the file you want to include has to be relative to the csproj file. – drphrozen Jul 30 '13 at 12:29
  • Some more description on how to use this file would be great. Do I have to compile it and save it as `.dll` or `.exe` ? Do I need any dependencies ( usually when installing VS you get the TFS exe with it's commands too, but what happens when I don't ? ) ... I would like to understand what is going on in there ... Also what about conflicts ? What if the file / files already exist in the `.csproj` ? – Roland Aug 01 '13 at 14:27
  • Also, this script is telling me that is not compatible with Win64 – Roland Aug 05 '13 at 06:28
  • It would be really great if anyone can tell me what nuget packages i need to use to make this snipet work. – SubqueryCrunch Jan 03 '21 at 17:52
  • 1
    Its been a while, but i think it should be: [Microsoft.TeamFoundationServer.ExtendedClient](https://www.nuget.org/packages/Microsoft.TeamFoundationServer.ExtendedClient/) @SubqueryCrunch – drphrozen Jan 04 '21 at 20:10
3

You can manualy edit project file to add files.

Project file has xml based format which you can edit symply. Try use grunt-text-replace for edit.

You can replace

</Project>

with

  <ItemGroup>
     <Compile Include="PythonProjectFactory.cs" />
  </ItemGroup>
</Project>
Vasiliy
  • 492
  • 3
  • 11
3

I know this is not the complete solution and I haven't tested yet, but it may help.

First I searched for a Powershell solution and found this questions:

They use EnvDTE the COM library containing the objects and members for Visual Studio core automation.

I couldn't find any resources how to use it, unless this from CodeProject Exporing EnvDTE.

Project project;

//path is a list of folders from the root of the project.
public void AddFromFile(List<string> path, string file) {
    ProjectItems pi = project.ProjectItems;
    for (int i = 0; i < path.Count; i++) {
        pi = pi.Item(path[i]).ProjectItems;
    }
    pi.AddFromFile(file);
}

//path is a list of folders from the root of the project.
public void AddFolder(string NewFolder, List<string> path) {
    ProjectItems pi = project.ProjectItems;
    for (int i = 0; i < path.Count; i++) {
        pi = pi.Item(path[i]).ProjectItems;
    }
    pi.AddFolder(NewFolder, 
       EnvDTE.Constants.vsProjectItemKindPhysicalFolder);
}

//path is a list of folders from the root of the project.
public void DeleteFileOrFolder(List<string> path, string item) {
    ProjectItems pi = project.ProjectItems;
    for (int i = 0; i < path.Count; i++) {
        pi = pi.Item(path[i]).ProjectItems;
    }
    pi.Item(item).Delete();
}

Since you want to use Grunt for automating your tasks. You could take a look at Edge - .NET for Node.js.

require('edge');

...

var fileAdder = edge.func(/*
  using EnvDTE;
  using EnvDTE80;
  using EnvDTE90;
  using EnvDTE100;

  using VSLangProj;
  using VsLangProj80;
  using VsLangProj90;
  using VsLangProj100;

  async (dynamic input) => { 
    // The C# code for adding the File to the project
  }
*/);    

grunt.registerTask('addFileVS', 
  'Adds new files to Visual Studio Project', 
   function (solutionname, projectname, filename){
     var input = {
       solution: solutionname,
       project: projectname,
       file: filename
     };
     fileAdder(input, function(error, results {...}));
   });
Community
  • 1
  • 1
Victor
  • 868
  • 8
  • 22
  • Will it be compatible with VS2012 ? In [this](http://www.codeproject.com/Articles/36219/Exploring-EnvDTE) article it does say that it's 2005 / 2008 only ... – Roland Aug 05 '13 at 06:37
  • I would be nice if you could extend a little bit on the desc, like continue the `fileAdder` func. I've also been looking at the edge plugin and I see that you can have the function outside grunt as a seperate `.cs` file. But how would I go about writing the code with the async ? I'm not exactly a C# expert :) – Roland Aug 05 '13 at 07:00
  • I haven't tried it yet. It may need some minor changes, but I'm pretty sure it works. The article was written before VS 2010, so I presume they haven't test it with higher version of VS. – Victor Aug 05 '13 at 07:04
  • I'll try it out, I just need to figure it out how to use this thing ... I just made a `.cs` file as such: [grunt.include.cs](http://snippi.com/s/fucb2h5) and I load it with edge: `var include = edge.func(path.resolve(__dirname, "grunt.include.cs"))`. Afterwards I register the [task](http://snippi.com/s/a26kdjb) in Grunt and when I try to run it I get some errors ... – Roland Aug 05 '13 at 07:35
  • It's also not able to find all of those libraries that you state: `using EnvDTE` ... – Roland Aug 05 '13 at 12:42
1

You can also run powershell script against your project file. We are using this for a long time.

AddContentToProject.ps1

    # Calling convension:
    #   AddContentToProject.PS1 "Mycsproj.csproj", "MyFolder/MyFile.txt"
    param([String]$path, [String]$include)

    $proj = [xml](Get-Content $path)
    [System.Console]::WriteLine("")
    [System.Console]::WriteLine("AddItemToProject {0} on {1}", $include, $path)

    # Create the following hierarchy
    #  <Content Include='{0}'>
    #  </Content>

    $xmlns = "http://schemas.microsoft.com/developer/msbuild/2003"
    $itemGroup = $proj.CreateElement("ItemGroup", $xmlns);
    $proj.Project.AppendChild($itemGroup);

    $contentNode = $proj.CreateElement("Content", $xmlns);
    $contentNode.SetAttribute("Include", $include);
    $itemGroup.AppendChild($contentNode)

    $proj.Save($path)

Then run this with powershell

.\AddContentToProject.ps1 "Mycsproj.csproj" "MyFolder/MyFile.txt"

Or this from command prompt

powershell -noexit "& ""C:\my_path\AddContentToProject.ps1"""
Kerem Demirer
  • 1,186
  • 2
  • 13
  • 24