'Programmatically access "Find All References" data in Visual Studio 2019

I am attempting to use automation in Visual Studio 2019 to help with repetitive tasks as part of a large code merge. I've researched quite a lot on this and am aware that using Roslyn can get me the information I need, but since I am performing other automation tasks in the IDE, I would rather persevere at extracting everything from the IDE as it is presented to me. I got quite a few bits working, but can't seem to be able to manipulate the Find All References tool windows. I can invoke the dialog using dte.ExecuteCommand("Edit.FindAllReferences"). I can also access the Window object from EnvDTE.Windows that corresponds to an existing "Find All References" window, but can't do much with it or the Window.Object property. I am also able to get the IVsWindowFrame object for an existing "Find All References" window, but again I am unable to do very much with it. The way to go appears to be to acquire IFindAllReferencesService but for me, GetService() always returns null. I've since tried the VCmd extension which lets you code macros against the IDE. I found that the macro code in this extension runs in the same process as the IDE - I don't know if it's for this reason or not, but the same GetService() code inside a macro returns a valid interface (Microsoft.VisualStudio.ErrorListPkg.FindAllReferencesService) which is what I need to progress being able to get references information out of a tool window.

I have the following code so far:

using EnvDTE80;
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Windows.Forms;
//using Microsoft.VisualStudio.Shell;
//using Microsoft.VisualStudio.Data.Framework;

namespace Util2
{
    public static class StackOverflowQuestion1
    {
        [DllImport("ole32.dll")]
        private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);
        [DllImport("ole32.dll")]
        private static extern void GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
        public static object GetInstanceBySolutionFileName(string FileName)
        {
            GetRunningObjectTable(0, out var rot);
            rot.EnumRunning(out var enumMoniker);
            enumMoniker.Reset();
            var fetched = IntPtr.Zero;
            var moniker = new IMoniker[1];
            CreateBindCtx(0, out var bindCtx);

            object comObject;
            string displayName;
            EnvDTE.DTE dte;
            while (enumMoniker.Next(1, moniker, fetched) == 0)
            {
                moniker[0].GetDisplayName(bindCtx, null, out displayName);
                if (!displayName.StartsWith("!VisualStudio.DTE.16.0:"))
                    continue;
                rot.GetObject(moniker[0], out comObject);
                dte = (EnvDTE.DTE)comObject;

                if (dte.Solution.FullName == FileName)
                    return comObject;
            }
            return null;
        }

        public static void Test()
        {
            Microsoft.VisualStudio.Shell.FindAllReferences.IFindAllReferencesService findAllReferencesService;
            //IServiceProvider serviceProvider;
            var dte = GetInstanceBySolutionFileName(@"C:\YOUR PROJECT PATH\YOUR_SOLUTION_FILE.sln");
            var serviceProvider = new Microsoft.VisualStudio.Shell.ServiceProvider((Microsoft.VisualStudio.OLE.Interop.IServiceProvider)dte);
            //serviceProvider = new Microsoft.VisualStudio.Data.Framework.ServiceProvider((Microsoft.VisualStudio.OLE.Interop.IServiceProvider)dte);
            findAllReferencesService = serviceProvider.GetService(
                typeof(Microsoft.VisualStudio.Shell.FindAllReferences.SVsFindAllReferences)) as
                Microsoft.VisualStudio.Shell.FindAllReferences.IFindAllReferencesService;
            if (findAllReferencesService == null)
                MessageBox.Show("NULL!");
            else
                MessageBox.Show("OK!");
            //MessageBox.Show(findAllReferencesService.GetType().ToString());
        }
    }
}

I am "supposed" to be using packages instead of automation from another application, but the concept of packages and their purpose don't fit what I am trying to achieve. I am trying to create a bunch of code snippets to help me automate a large merge, after that I will not need it. Also, there is a possibility I will need to manipulate 2 VS instances, so even the fact that the code works inside a package, it should not work if that package attempts to hook into another VS instance (i.e. it will have the same problem as my application).

For what it's worth, here's the code that can get me the IVsWindowFrame for the Find All Reference window(s):

            var shell = (IVsUIShell)serviceProvider.GetService(typeof(SVsUIShell));
            var guid = new Guid("a80febb4-e7e0-4147-b476-21aaf2453969");
            shell.FindToolWindowEx((uint)__VSFINDTOOLWIN.FTW_fFindFirst, ref guid, 0, out var windowFrame);

Note, the GUID comes from EnvDTE.Windows.Item().ObjectKind for a references window. Here is a little more about such a window, as an example:

  • Caption='txtTest_TextChanged' references
  • Kind=Tool
  • ObjectKind={A80FEBB4-E7E0-4147-B476-21AAF2453969}
  • Type=vsWindowTypeToolWindow

As a hack for now, I will call the extension's macro from my application (e.g. VCmd.Command01) which in turn will locate the Find All References window (or search for references for symbol at cursor) then write the results somewhere (e.g. file) and my application would read the results.

EDIT: My hack didn't work as planned and even though it had mileage in the end, I found a quicker way (sort of). After executing command Edit.FindAllReferences, ensure the Visual Studio window itself is focused, the 'Find references' tool/tab would be in a focused state, so I sent it a CTRL+A then CTRL+C using SendInput - all the details I need are in the clipboard which achieves my goal. Similar could be achieved by sending keys to navigate the box, but I haven't got the need for that yet. Obviously this is quite hacky and being able to use IFindAllReferences from another application would be preferrable. Using the macro extension VCMD, I was able to acquire IFindAllReferencesService ref, and was able to call StartSearch(). Looking in more detail, StartSearch merely initialises the window that it returns which is returned as IFindAllReferencesWindow. I haven't tried this, but I think it's possible to hold on to the IFindReferencesWindow interface returned by StartSearch (passing it the exact same label name as it would appear in the IDE e.g. "'button7' references"), then perform Edit.FindAllReferences command, then access the IFindAllReferencesWindow's properties to hopefully get the data. But there were other difficulties...so Clipboard method wins for now :)



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source