Opening a PowerShell Prompt at the TargetPath of the Selected Project

When developing an application, I usually spend a lot of time in the command-line. For this, I use PowerShell, that gives me access to all my scripts that I have built over time. One special need that I have is to perform some commands in the target path of the selected Project in Solution Explorer.

For instance, in the product we’re building, that has many dependencies, we are controlling exactly which binding redirects need to be present in our executables’ App.config file.

We have built a simple script that processes the contents of a folder and determines from there the exact list of binding redirects that need to be written in the project’s App.config file, along with appropriate comments specifying which dependencies do need these redirects.

Therefore, when developing with Visual Studio, I constantly have to drop to PowerShell and change the location of the current workfing folder in order to run my scripts. For this, I use the “Open Folder in Explorer” context-menu command to open Explorer. I then copy the full path to the project and paste that into PowerShell to issue a Set-Location command.

Over time, this is cumbersome.

So I set out to build a simple Visual Studio Extension (VSIX) that does exactly that. It allows to open a PowerShell command prompt initialized at the location of the selected Project’s TargetPath.

A simple Visual Studio Extension

In order to help me build my first Visual Studio Extension, I followed the talk about Visual Studio Extensibility, that Mads Kristensen has given during //Build 2016. This talk walks you through building a Visual Studio Extension, publishing it on GitHub and setting up a continuous integration build on AppVeyor so that new versions can be published in a minimum of time to the official Visual Studio Gallery where extensions live.

Of course, this being my first Visual Studio Extension, I still had to figure out how to determine the selected project in Solution Explorer, then how to find its TargetPath and, finally, how to launch a PowerShell prompt initialized with a specific folder.

So, here are the three pieces of the puzzle:

How to find the selected Project in Solution Explorer

This has been the hardest part for me to solve. Indeed, Visual Studio has many extensibility points and it is not easy to find the correct way to achieve a particular result. Fortunately, many developers have similar needs when building their extensions, so I was able to coble together a working solution from multiple sources.

The following code shows how to do this:

public sealed class SolutionExplorer
{
    public static Project GetSelectedProject()
    {
        return GetSelectedItem() as Project;
    }

    private static object GetSelectedItem()
    {
        var monitorSelection = (IVsMonitorSelection)Package.GetGlobalService(typeof(SVsShellMonitorSelection));

        IntPtr hierarchyPointer;
        IntPtr selectionContainerPointer;
        IVsMultiItemSelect multiItemSelect;
        uint projectItemId;

        monitorSelection.GetCurrentSelection(out hierarchyPointer,
            out projectItemId,
            out multiItemSelect,
            out selectionContainerPointer);

        using (var container = TypedObjectForIUnknown<ISelectionContainer>.Attach(selectionContainerPointer))
        using (var hierarchy = TypedObjectForIUnknown<IVsHierarchy>.Attach(hierarchyPointer))
        {
            object selection = null;
            ErrorHandler.ThrowOnFailure(
                hierarchy.Instance.GetProperty(
                    projectItemId,
                    (int)__VSHPROPID.VSHPROPID_ExtObject,
                    out selection)
                );

            return selection;
        }
    }
}

This code uses the IVsMonitorSelection extensibility interface that is used to obtain some informations about the project hierarchy.

This code also takes advantage of the TypedObjectForIUnknown helper class that I have written in order to make it easier to consume and dispose of extensibility interfaces around the using statement.

How to find a project’s Target Path

Once the full path to the project’s .csproj file has been determined, I figured I could leverage MSBuild to give me its TargetPath, since this information is specified in the project as part of a PropertyGroup.

It turns out this is quite easy to do, as the following code demonstrates:

  public static string GetTargetPath(string csproj)
  {
      const string propertyName = "TargetPath";
      return GetProperty(csproj, propertyName);
  }

  public static string GetProperty(string csproj, string propertyName)
  {
      var properties = new Dictionary<string, string>();

      // load MSBuild project and evaluate its TargetPath
      // based upon the selected configuration

      using (var collection = new ProjectCollection())
      {
          var project = new Project(
              csproj
              , properties
              , null
              , collection
              , ProjectLoadSettings.Default
              );

          var property = project.Properties
              .Where(p => p.Name == propertyName)
              .Select(p => p.EvaluatedValue)
              .SingleOrDefault()
              ;

          return property;
      }
  }

How to launch PowerShell at a specified location

This is the easiest part of building this extension! For this, it is sufficient to run PowerShell with an initial command that performs a Set-Location to the specified folder. However, path can have spaces in them and it is somewhat hard to issue commands with the proper escaping characters for the different layers of processing.

In order to make it easier on me, I chose to use the EncodedCommand command-line switch of PowerShell, that allows to specified a command as a Base64 encoded UTF-16 string.

The code looks like this:


// run PowerShell prompt at the target location

const string program = @"%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe";
const string arguments = "/nologo /noexit /encodedCommand {0}";

var command = $"Set-Location \"{targetFolder}\"";
var encodedCommand = Convert.ToBase64String(Encoding.Unicode.GetBytes(command));

var prog = Environment.ExpandEnvironmentVariables(program);
var options = String.Format(arguments, encodedCommand);

var process = new System.Diagnostics.Process();
process.StartInfo.FileName = prog;
process.StartInfo.Arguments = options;
process.StartInfo.WorkingDirectory = targetFolder;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = false;
process.Start();
This entry was posted in Visual Studio (VSIX) and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s