I created an application to run, but build an External Tool from the application for Visual Studio. Up until 15.4.0 the application worked fine, but they modified the registry. Based on Microsoft's changes, the External Tools are saved under this sub key:
SOFTWARE\Microsoft\VisualStudio\14.0_Config\External Tools
When you observe the key, you clearly see folders (sub keys) of tools, Guid, Error Lookup, and a couple others that are included with Visual Studio. The issue though, if you manually create the tool in Visual Studio the key information is dumped directly to the root External Tool key.
So if I do either of these approaches:
var user = RegistryKey.OpenBaseKey(RegistryHive.CurrenUser, RegistryView.Default);
var tools = user.OpenSubKey(@"SOFTWARE\Microsoft\VisualStudio\14.0_Config\External Tools", true);
var path = Environment.GetCommandLineArgs()[0];
tools.SetValue("ToolArg", "$(ItemPath)");
tools.SetValue("ToolCmd", path);
tools.SetValue("ToolDir", String.Empty);
tools.SetValue("ToolOpt", 0x12);
tools.SetValue("ToolsSourceKey", String.Empty);
tools.SetValue("ToolTitle", "My Tool");
var user = RegistryKey.OpenBaseKey(RegistryHive.CurrenUser, RegistryView.Default);
var tools = user.OpenSubKey(@"SOFTWARE\Microsoft\VisualStudio\14.0_Config\External Tools", true);
var sub = tools.CreateSubKey("MyTool");
var path = Environment.GetCommandLineArgs()[0];
sub.SetValue("ToolArg", "$(ItemPath)");
sub.SetValue("ToolCmd", path);
sub.SetValue("ToolDir", String.Empty);
sub.SetValue("ToolOpt", 0x12);
sub.SetValue("ToolsSourceKey", String.Empty);
sub.SetValue("ToolTitle", "My Tool");
The tool doesn't appear in the list, or toolbar. Is there something different for Visual Studio 2017 15.5.* that makes this no longer work from code? To make matters worst, the key doesn't always appear when created in Visual Studio 2017 manually.
In Visual Studio 2017, External Tools are stored in a private registry hive in the user's local application data folder. If you run Sysinternals Process Monitor tool, you'll see Visual Studio reading/writing to a key that starts with \REGISTRY\A\
- that's how you know it's a private registry hive. To update them, you will need to load that registry hive by P/Invoking RegLoadAppKey
and attaching to the resulting handle. An example of that can be found here:
RegLoadAppKey working fine on 32-bit OS, failing on 64-bit OS, even if both processes are 32-bit
The title may seem misleading at first, but the example given in the question shows exactly how to call RegLoadAppKey
and open a sub key underneath.
The next thing you'll have to contend with is finding the private registry hive. Visual Studio stores the private registry hive in a sub folder of the user's local application data folder. The sub folder name will start with Microsoft\VisualStudio\15.0_
and will then be followed by a 32-bit hexadecimal value. I'm not really sure what that value is, or how to gracefully discover it. It's different for each user. My approach was to select the newest folder that starts with "15.0" and assume it's correct. If someone has a better way to identify this folder, I would love to see it.
I've dubbed the combination of version number and hexadecimal string a "version tag". You'll need to keep track of it, because the version tag will be used again as a sub key in the private registry hive.
Putting it all together, I created a VisualStudioContext
class that locates the private registry hive and loads the root key.
public class VisualStudioContext : IDisposable
{
public string VersionTag { get; }
public string UserFolder { get; }
public string PrivateRegistryPath { get; }
public SafeRegistryHandle RegistryHandle { get; }
public RegistryKey RootKey { get; }
private static readonly Lazy<VisualStudioContext> LazyInstance = new Lazy<VisualStudioContext>(() => new VisualStudioContext());
public static VisualStudioContext Instance => LazyInstance.Value;
private VisualStudioContext()
{
try
{
string localAppDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string vsFolder = $"{localAppDataFolder}\\Microsoft\\VisualStudio";
var vsFolderInfo = new DirectoryInfo(vsFolder);
DateTime lastDateTime = DateTime.MinValue;
foreach (DirectoryInfo dirInfo in vsFolderInfo.GetDirectories("15.0_*"))
{
if (dirInfo.CreationTime <= lastDateTime)
continue;
UserFolder = dirInfo.FullName;
lastDateTime = dirInfo.CreationTime;
}
if (UserFolder == null)
throw new Exception($"No Visual Studio folders found in \"{vsFolder}\"");
}
catch (Exception ex)
{
throw new Exception("Unable to open Visual Studio folder", ex);
}
VersionTag = Path.GetFileName(UserFolder);
PrivateRegistryPath = $"{UserFolder}\\privateregistry.bin";
int handle = RegistryNativeMethods.RegLoadAppKey(PrivateRegistryPath);
RegistryHandle = new SafeRegistryHandle(new IntPtr(handle), true);
RootKey = RegistryKey.FromHandle(RegistryHandle);
}
public void Dispose()
{
RootKey?.Close();
RegistryHandle?.Dispose();
}
public class Exception : ApplicationException
{
public Exception(string message) : base(message)
{
}
public Exception(string message, Exception innerException) : base(message, innerException)
{
}
}
internal static class RegistryNativeMethods
{
[Flags]
public enum RegSAM
{
AllAccess = 0x000f003f
}
private const int REG_PROCESS_APPKEY = 0x00000001;
// approximated from pinvoke.net's RegLoadKey and RegOpenKey
// NOTE: changed return from long to int so we could do Win32Exception on it
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegLoadAppKey(String hiveFile, out int hKey, RegSAM samDesired, int options, int reserved);
public static int RegLoadAppKey(String hiveFile)
{
int hKey;
int rc = RegLoadAppKey(hiveFile, out hKey, RegSAM.AllAccess, REG_PROCESS_APPKEY, 0);
if (rc != 0)
{
throw new Win32Exception(rc, "Failed during RegLoadAppKey of file " + hiveFile);
}
return hKey;
}
}
}
You can use it open the External Tools key like this:
using (var context = VisualStudioContext.Instance)
{
RegistryKey keyExternalTools =
context.RootKey.OpenSubKey($"Software\\Microsoft\\VisualStudio\\{context.VersionTag}\\External Tools", true);
// Do something interesting here
}