0

I am developing a new version of a Visual Studio extension. In the old version, the hotkeys were stored in the registry and I would like to import these settings into the new version of the software.

The new version uses .vsct format for defining menu commands and you can assign the hotkeys in the .vsct file. However, I can't use this mechanism, as I would like to import the user settings from the registry, whiles the .vsct is a static description.

When my VSPackage is initialized, I can read the old hotkeys from the registry, but I have problems assigning them to my menu commands defined in the .vsct file. I can get a hold on the OleMenuCommand interface and OleMenuCommandService, but they have no property where they can accept key bindings.

How one can assign hotkeys to menu commands programmatically?

A clunky workaround would be that the installer imports the hotkeys, writes them into the .vsct file, compiles the file and puts the .cto into the MyPackage.Resources.dll during the installation. But I'd rather not resort to this...

Cœur
  • 37,241
  • 25
  • 195
  • 267
Shakaron
  • 1,027
  • 10
  • 24

3 Answers3

0

Though I define my commands through the .vsct file, I still can access them via the EnvDTE.Command interface.

// The commandGuid has to be the same GUID you use in the .vsct file, HOWEVER, you have to include the braces here
Command command = dte.Commands.Item(commandGuid, commandId);
command.Bindings = new object[] { "Global::Ctrl+Alt+Shift+1" };

It works. It is saved and remembered the next time Visual Studio starts up, so there is no need to import the old hotkeys every time.

Shakaron
  • 1,027
  • 10
  • 24
0

Since I'm not allowed to comment [despite my 50 years of systems-level programming experience, including 3 books for MS Press :( ], I have to ask this way for Shakaron to post some more of his code. I added a short tree of menu commands to the VS 2015 Tools menu (1-4-1 elements). An enumeration of dte.Commands didn't show any of them. Calling Commands.Item with a string (or an "object" to which I'd assigned a string) containing the GUID, both with and without {}, generated an invalid argument exception.

So, either the EnvDTE80 interface is incredibly fragile, the documentation is wildly wrong, or else Shakaron's solution has some more magic that we haven't seen yet. And I was so hopeful that this would be the last piece of my puzzle...

Walter Oney
  • 163
  • 5
  • Did you use .vsct file to add the menu commands (which compiles to a .cto file) or did you do it programmatically via the DTE interface? – Shakaron Sep 21 '15 at 15:32
  • **"System.ArgumentException: The parameter is incorrect. (Exception from HRESULT: 0x80070057 (E_INVALIDARG))"** is thrown when *dte.Commands.Item* cannot find the item with the provided *commandGuid* and/or *commandId*. It might be a bit silly to throw an exception instead of returning null, but that's not the point right now. You really have to make sure you use the correct *commandGuid* and *commandId*. Since you said they are now shown when you try to enumerate the *dte.Commands* I guess something must have gone wrong with the registration of the commands. – Shakaron Sep 21 '15 at 15:37
  • Since you can't comment I'd suggest you raise your own question and post "another answer" here, so I'll get a notification and can look at your problem there. Meanwhile, please consider the comments above. – Shakaron Sep 21 '15 at 15:44
0

Answering Shakaron's questions:

  1. I used the .vsct file to add 1 menu with 3 commands and 1 submenu, which in turn had 1 command.

  2. Calling Commands.Item with a guid and an index argument caused an apparently identical invalid argument exception. I was able to enumerate the collection, but my commands were not in the set.

  3. I also couldn't compile your code for the right-hand-side of the Bindings assignment. But, of course, I didn't get that far because I couldn't find the command item in the first place.

  4. There has to be a version dependency at work here. I don't doubt that your code worked a year ago, but I ran into the problems I described using a fresh-out-of-the-virtual-box copy of VS 2015 running on Win 10.

Walter Oney
  • 163
  • 5
  • Ah, so you're using VS2015. I haven't tested my code with that versioun of Visual Studio yet... However I tested it with 2010, 2012, 2013 and it works all right (actually I copy-pasted the assignment `command.Bindings = new object[] { "Global::Ctrl+Alt+Shift+1" };` and it not only compiles, but pressing +++<1> executed the assigned command just fine). Although I haven't tested my code with VS2015 personally, one of my colleague has compiled and tested our product in VS2015 where I use a similar assignment and he never mentioned that the code I wrote did not work there. – Shakaron Sep 23 '15 at 09:36
  • Another idea might be that you try to iterate through the items before they are actually created. I would create an extra button on the UI (extra menu command) and on the executed event I would try to find the items. If you can activate the menu command, they must be fully initialized and present in the DTE.Commands enumeration. We can access the menu commands from Package.Initialize() method. You should be able to find them as well. – Shakaron Sep 23 '15 at 09:40
  • A VS2013 sample is available to download (until I remove it) here: https://drive.google.com/file/d/0B_mZbYod0BSIaW10blFycmFRTms/view?usp=sharing I know you're using VS2015, but I don't have that installed on my machine just yet. Look at bottom of `DTEHotkeyBindingSamplePackage.Initialize()`. Everything works fine. Even you can press +++<1> to bring up the basic window generated by the VS sample. Please not, that I had to add `[ProvideAutoLoad(VSConstants.UICONTEXT.NoSolution_string)]` on the package to force loading the commands without fist clicking on them on the UI. – Shakaron Sep 23 '15 at 09:57
  • The magic line of code is this one: "DTE dte = GetService(typeof(SDTE)) as DTE;" Using that, I can find the command in the collection. The fact that this works points out a deep flaw in the way Microsoft has documented the .NET API. In the documentation for an interface, they rarely describe how to get a reference to that interface. The doc for the DTE2 interface says to write this code: "EnvDTE80.DTE2 dte2; dte2 = (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal. GetActiveObject("VisualStudio.DTE.11.0");" So where did you learn to use the GetService call? – Walter Oney Sep 23 '15 at 13:39
  • There are a lot of useful sites to learn about Visual Studio programming. I've been doing this for a couple of years now so I've picked things up and my boss helped a lot too. :) But here is a small collection of how to get the DTE object: http://stackoverflow.com/questions/19087186/how-to-acquire-dte-object-instance-in-a-vs-package-project (4 methods are mentioned, one in the question itself). So browse the net, read the **MSDN Documentation**, look for **MZTools** (e.g.: http://www.mztools.com/articles/2013/MZ2013029.aspx) and just in general, check for multiple solutions for a problem. :) – Shakaron Sep 24 '15 at 09:41
  • By the way GetActiveObject should work. We use that (as well) in our live code and the product I'm working on is used by quite a lot of people. The trick with `GetActiveObject` is that you get the right running VS instance, so version number and other properties should be checked. It is way safer to use `this.GetService` if you're in the package or `Package.GetGlobalService` static method if you are outside of your package class. – Shakaron Sep 24 '15 at 09:46