Helixoft Blog

Peter Macej - lead developer of VSdocman - talks about Visual Studio tips and automation

Creating EnvDTE.DTE for VS 2017 from outside the IDE

You can automate Visual Studio by getting or creating an instance of EnvDTE.DTE. For example, you can create a new VS 2015 instance with the following code:

System.Type dteType = Type.GetTypeFromProgID("VisualStudio.DTE.14.0", true);
EnvDTE.DTE dte = (EnvDTE.DTE)System.Activator.CreateInstance(dteType);

Each Visual Studio version has its own progID. For example, VS 2015 has "VisualStudio.DTE.14.0" and VS 2013 has "VisualStudio.DTE.12.0".

There's also one special progID "VisualStudio.DTE". It is an alias for the latest installed or updated VS version. It doesn't represent the highest installed version and it may change over time. You should keep this in mind. Let's take an example. On a clean machine, you install VS 2015. The "VisualStudio.DTE" will create DTE for VS 2015. Then you install VS 2013, which will cause that the "VisualStudio.DTE" will create DTE for VS 2013. After that, an update for VS 2015 is installed and the "VisualStudio.DTE" will represent VS 2015 again. As you can see, using the "VisualStudio.DTE" is not very reliable. Using versioned "VisualStudio.DTE.X.Y" is safe, provided that the Visual Studio version X.Y is installed.

Let's look at VS 2017 now. It has its own "VisualStudio.DTE.15.0", so you may think there's no problem and we can use it as with previous VS versions. This is true only partially. The problem is that starting with VS 2017, we can have multiple instances of the same version installed on the same machine. For example, it is possible to install VS 2017 Community and Enterprise at the same time. Now, the "VisualStudio.DTE.15.0" starts behave similarly to "VisualStudio.DTE". Which instance of VS 2017 will be created? If there's only one instance of VS 2017 installed, there's no problem. If there are multiple instances installed, probably (like with "VisualStudio.DTE") the one that was installed or updated as the last,  will be used. I don't have a machine with multiple versions installed, I can only guess. The early releases of VS 2017 RC didn't support automation at all. The latest VS 2017 RC update caused it to start working. So it's very likely that VS 2017 updates will update the "VisualStudio.DTE.15.0" registration.

If it doesn't matter which VS 2017 instance will be used, you can create it with "VisualStudio.DTE.15.0" progID. 

If you however require specific VS 2017 instance, you need to do some more work. As suggested by Microsoft, start the specific application using the exact path desired, then get the EnvDTE object from the running object table.

Here's a C# example of how to do it. First, it creates an invisible instance of devenv.exe, whose full path is provided. As you can see, the "magic" undocummented "-Embedding" parameter is passed to it. It will cause the IDE to be in an invisible state with no user interaction allowed. This parameter is automatically passed also when you call System.Activator.CreateInstance(dteType) so it doesn't hurt if we use it too. Having the devenv process, we retrieve the DTE object from the running object table.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text.RegularExpressions;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                // Register the IOleMessageFilter to handle any threading errors.
                // Not implemented in this example for simplicity.
                // See https://msdn.microsoft.com/en-us/library/ms228772
                //MessageFilter.Register();

                // create DTE
                string devenvPath;
                //devenvPath = @"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe";
                devenvPath = @"C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe"; // change it to your actual path
                EnvDTE.DTE dte = CreateDteInstance(devenvPath);

                if (dte != null)
                {
                    // print edition
                    Console.WriteLine($"Edition: {dte.Edition}");
                    // show IDE
                    dte.MainWindow.Visible = true;
                    dte.UserControl = true;
                }
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString());
            }
            finally {
                // turn off the IOleMessageFilter.
                //MessageFilter.Revoke();
            }
        }


        /// Creates and returns a DTE instance of specified VS version.
        /// The full path to the devenv.exe.
        /// DTE instance or  if not found.
        public static EnvDTE.DTE CreateDteInstance(string devenvPath)
        {
            EnvDTE.DTE dte = null;
            Process proc = null;

            // start devenv
            ProcessStartInfo procStartInfo = new ProcessStartInfo();
            procStartInfo.Arguments = "-Embedding";
            procStartInfo.CreateNoWindow = true;
            procStartInfo.FileName = devenvPath;
            procStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            procStartInfo.WorkingDirectory = System.IO.Path.GetDirectoryName(devenvPath);

            try
            {
                proc = Process.Start(procStartInfo);
            }
            catch (Exception ex)
            {
                return null;
            }
            if (proc == null)
            {
                return null;
            }

            // get DTE
            dte = GetDTE(proc.Id, 120);

            return dte;
        }


        /// 
        /// Gets the DTE object from any devenv process.
        /// 
        /// 
        /// After starting devenv.exe, the DTE object is not ready. We need to try repeatedly and fail after the
        /// timeout.
        /// 
        /// 
        /// Timeout in seconds.
        /// 
        /// Retrieved DTE object or  if not found.
        /// 
        private static EnvDTE.DTE GetDTE(int processId, int timeout)
        {
            EnvDTE.DTE res = null;
            DateTime startTime = DateTime.Now;

            while (res == null && DateTime.Now.Subtract(startTime).Seconds < timeout)
            {
                Thread.Sleep(1000);
                res = GetDTE(processId);
            }

            return res;
        }


        [DllImport("ole32.dll")]
        private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);


        /// 
        /// Gets the DTE object from any devenv process.
        /// 
        /// 
        /// 
        /// Retrieved DTE object or  if not found.
        /// 
        private static EnvDTE.DTE GetDTE(int processId)
        {
            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.
                    }

                    Regex monikerRegex = new Regex(@"!VisualStudio.DTE\.\d+\.\d+\:" + processId, RegexOptions.IgnoreCase);
                    if (!string.IsNullOrEmpty(name) && monikerRegex.IsMatch(name))
                    {
                        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 runningObject as EnvDTE.DTE;
        }

    }
}

 

Add comment


Security code
Refresh

 

Start generating your .NET documentation now!
DOWNLOAD
Free, fully functional trial