Dynamically add commands to your VSIP Package

The set of commands that you can expose in a Visual Studio Package is fixed and defined by the .ctc file that you compile and include into your satellite dll.

But what if after installation you need to create more commands? There is a solution to this problem. It implies having the ctc.exe (Command table compiler) from the VSIP distribution bits be distributed with your VSIP package. But that’s the only piece of bits that you will need from the VSIP distribution.

Let’s begin, at runtime your package will need to create a new CTC file and have the CTC.EXE (the ctc compiler tool) compile it into a cto file.

Notice that you are creating a dependency to the VSIP bits by having to invoke ctc.exe. However, with a little extra effort that will be the only dependency to the VSIP bits.

Normally a CTC file uses the command preprocessor from Visual C++ compiler: cl.exe. You don’t want to include the C++ compiler with your VSIP package right? Well the trick is to fool the ctc compiler with a dummy command preprocessor. This small batch script will be enough:

@echo off

echo #line 1 %4

type %4

Because we are not using the real preprocessor, then our generated CTC file cannot use any symbols that are defined in another inclusion file. For example, you cannot use the header file "vsshlids.h", instead you need to generate your CTC file without the help of any macro replacement.

For example, instead of using replacement macros like in here:

CMDS_SECTION MyPackageGUID

MENUS_BEGIN

MENUS_END

NEWGROUPS_BEGIN

MyGroupGuid:0x0100, guidSHLMainMenu:IDM_VS_MENU_TOOLS,0x600;

NEWGROUPS_END

BUTTONS_BEGIN

MyGroupGuid:0x0100,MyGroupGuid:0x0100,0x0400, OI_NOID,0x00000001,0x00000000,"&My New Command";

BUTTONS_END

BITMAPS_BEGIN

BITMAPS_END

CMDS_END

CMDUSED_SECTION

CMDUSED_END

CMDPLACEMENT_SECTION

CMDPLACEMENT_END

VISIBILITY_SECTION

VISIBILITY_END

KEYBINDINGS_SECTION

KEYBINDINGS_END

You will have to use the real value and generate this:

CMDS_SECTION { 0x77D93A80, 0x73FC, 0x40F8, { 0x87, 0xDB, 0xAC, 0xD3, 0x48, 0x29, 0x64, 0xB2 } }

MENUS_BEGIN

MENUS_END

NEWGROUPS_BEGIN

{ 0x96119584, 0x8C4B, 0x4910, { 0x92, 0x11, 0x71, 0xD4, 0x8F, 0xA5, 0x9F, 0xAF } }:0x0100, { 0xD309F791, 0x903F, 0x11D0, { 0x9E, 0xFC, 0x00, 0xA0, 0xC9, 0x11, 0x00, 0x4F } }:0x0413,0x600;

NEWGROUPS_END

BUTTONS_BEGIN

{ 0x96119584, 0x8C4B, 0x4910, { 0x92, 0x11, 0x71, 0xD4, 0x8F, 0xA5, 0x9F, 0xAF } }:0x0100,{ 0x96119584, 0x8C4B, 0x4910, { 0x92, 0x11, 0x71, 0xD4, 0x8F, 0xA5, 0x9F, 0xAF } }:0x0100,0x0400,{ 0xD309F794, 0x903F, 0x11D0, { 0x9E, 0xFC, 0x00, 0xA0, 0xC9, 0x11, 0x00, 0x4F } }:0x02EA,0x00000001,0x00000000,"&My New Command";

BUTTONS_END

BITMAPS_BEGIN

BITMAPS_END

CMDS_END

CMDUSED_SECTION

CMDUSED_END

CMDPLACEMENT_SECTION

CMDPLACEMENT_END

VISIBILITY_SECTION

VISIBILITY_END

KEYBINDINGS_SECTION

KEYBINDINGS_END

Yes, I know it looks ugly (actually both files look really ugly), but it will be auto generated. Notice that the guid of the CMDS_SECTION must match the guid of your VSIP package. This is a very important detail; the guid must match so that VS finds a loaded package with that guid.

Now you are ready to invoke ctc and create your own cto file, you will invoke like this:

ctc.exe MyCTCFile.ctc MyCTCFile.cto –Ccpp.bat

where MyCTCFile.ctc if your generated ctc file and cpp.bat is the dummy preprocessor.

Ok, now you have a cto file, but how do make VS load it? The answer: 1. by creating a new satellite dll and 2. Registering it as the satellite dll of a non existent or dummy package.

1. To create a new satellite dll, you can use the exiting satellite dll from your VSIP Package (If you package does not have a satellite dll, you will have to create an empty one).

Then you can use the BeginUpdateResource and UpdateResource functions to create you new satellite dll from the original satellite dll.

internal sealed class NativeMethods

{

[DllImport("kernel32.dll", SetLastError = true)]

public static extern IntPtr BeginUpdateResource(string pFileName, Int32 bDeleteExistingResources);

[DllImport("kernel32.dll", SetLastError = true)]

public static extern Int32 EndUpdateResource(IntPtr hUpdate, Int32 fDiscard);

[DllImport("kernel32.dll", SetLastError = true)]

public static extern Int32 UpdateResource(IntPtr hUpdate, string lpType, Int16 lpName, Int16 wLanguage, byte[] lpData, Int32 cbData);

}

File.Copy("OrginalSatellite.dll", SatelliteDllFileName);

IntPtr hUpdate = IntPtr.Zero;

using (FileStream ctoFile = new FileStream(tempCTO, FileMode.Open,FileAccess.Read))

{

using (BinaryReader ctoData = new BinaryReader(ctoFile))

{

byte[] ctoArray = ctoData.ReadBytes((int)ctoFile.Length);

hUpdate = NativeMethods.BeginUpdateResource(this.SatelliteDllFileName, 1);

try

{

if (hUpdate.Equals(IntPtr.Zero))

{

Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

}

if (NativeMethods.UpdateResource(

hUpdate, "CTMENU", (Int16)ResourceId,

(short)CultureInfo.CurrentUICulture.LCID,

ctoArray, (Int32)ctoArray.Length) != 1)

{

Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

}

}

finally

{

if (!hUpdate.Equals(IntPtr.Zero))

{

NativeMethods.EndUpdateResource(hUpdate, 0);

hUpdate = IntPtr.Zero;

}

}

}

}

In this code, the string "OrginalSatellite.dll" represents the location of the satellite dll of your package. The variable tempCTO is the file name of cto file created by the invocation of ctc.exe.

2. Ok, you got a new satellite dll, but as I said before, we need a dummy package for this new satellite dll. Basically you will need to create a new guid, this new guid can be stolen from a guid used in the generated ctc file, I am creating a new command group in my ctc file, so I am using the same guid for the dummy package.

Register the dummy package by creating a new key under HKLM\Software\Microsoft\VisualStudio\Packages:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\Packages\{96119584-8C4B-4910-0x9211-71D48FA59FAF}]

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\Packages\{96119584-8C4B-4910-0x9211-71D48FA59FAF}\SatelliteDll]

"DllName"="MyNewSatelliteDllName.dll"

"Path"="C:\\Path\\To\\My\\New\\SatelliteDll"

As with any other satellite with menu resources we need to register it in the HKLM\Software\Microsoft\VisualStudio\\Menus

Just write a new string entry with the dummy package guid and the value as “,ResourceId,1”, where ResourceId must match the id that you use in the call to UpdateResource:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\Menus]

"{96119584-8C4B-4910-0x9211-71D48FA59FAF}"=",101,1"

Now you must exit devenv.exe and run devenv.exe /setup again so that VS rebuild the command table.

That’s it. Your VSIP package has now more commands attach to it.

Improvements:

- Remove the dependency to the ctc.exe file, this will require knowledge on the binary representation of a cto file. I suspect that the format is not complicated and very straight forward.

- This procedure requires you to run devenv.exe /setup, is there a way to update the command table of VS at runtime?