How to get DTE from Visual Studio process ID?

DTE is an automation framework that is used to programmatically control Visual Studio, often from another process. It internally uses COM remoting to execute commands from another process on the VS UI thread.

A while back I have written about How to start Visual Studio programmatically and get the DTE object to control the devenv.exe process. But how to get the DTE object to automate an already running Visual Studio instance? Or how to get the DTE object to automate Visual Studio started from an experimental hive (using the /rootsuffix command line option)?

Here’s the code (need to reference EnvDTE, which is a PIA, no pun intended):

 using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using EnvDTE;
 
public class AutomateVS
{
    [DllImport("ole32.dll")]
    private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);
 
    public static DTE GetDTE(int processId)
    {
        string progId = "!VisualStudio.DTE.10.0:" + processId.ToString();
        object runningObject = null;
 
        IBindCtx bindCtx = null;
        IRunningObjectTable rot = null;
        IEnumMoniker enumMonikers = null;
 
        try
        {
            Marshal.ThrowExceptionForHR(CreateBindCtx(reserved: 0, ppbc: out bindCtx));
            bindCtx.GetRunningObjectTable(out rot);
            rot.EnumRunning(out enumMonikers);
 
            IMoniker[] moniker = new IMoniker[1];
            IntPtr numberFetched = IntPtr.Zero;
            while (enumMonikers.Next(1, moniker, numberFetched) == 0)
            {
                IMoniker runningObjectMoniker = moniker[0];
 
                string name = null;
 
                try
                {
                    if (runningObjectMoniker != null)
                    {
                        runningObjectMoniker.GetDisplayName(bindCtx, null, out name);
                    }
                }
                catch (UnauthorizedAccessException)
                {
                    // Do nothing, there is something in the ROT that we do not have access to.
                }
 
                if (!string.IsNullOrEmpty(name) && string.Equals(name, progId, StringComparison.Ordinal))
                {
                    Marshal.ThrowExceptionForHR(rot.GetObject(runningObjectMoniker, out runningObject));
                    break;
                }
            }
        }
        finally
        {
            if (enumMonikers != null)
            {
                Marshal.ReleaseComObject(enumMonikers);
            }
 
            if (rot != null)
            {
                Marshal.ReleaseComObject(rot);
            }
 
            if (bindCtx != null)
            {
                Marshal.ReleaseComObject(bindCtx);
            }
        }
 
        return (DTE)runningObject;
    }
 
    static void Main(string[] args)
    {
        var devenv = System.Diagnostics.Process.Start("devenv.exe");
 
        DTE dte = null;
        do
        {
            System.Threading.Thread.Sleep(2000);
            dte = GetDTE(devenv.Id);
        }
        while (dte == null);
 
        dte.ExecuteCommand("View.CommandWindow");
        dte.StatusBar.Text = "Hello World!";
        System.Threading.Thread.Sleep(2000);
        dte.ExecuteCommand("File.Exit");
        devenv.WaitForExit();
        Marshal.ReleaseComObject(dte);
    }
}

When Visual Studio starts (regardless if it was passed the /rootsuffix command line argument), it places itself onto the Running Object Table (aka ROT). We know the moniker that the Visual Studio process will use to identify itself (it’s !VisualStudio.DTE.10.0:1234, where 1234 is the devenv.exe process ID). We then enumerate though all the objects in the ROT and find the one with the right moniker.

Then you can just keep using the DTE object as if you’re writing a macro (Visual Studio macros also use DTE to automate VS).