VSIX window - key shortcut to execute ICommand

2019-08-02 08:48发布

问题:

Having a Visual Studio extension (VSIX) project: In Window we got UserControl, which have Button binded to some ICommand. This works perfectly as expected, however I would like to attach a key shortcut (e.g.:CTRL + S) which would fire the same Command.

I have checked several questions, in which I found the most useful this code:

<UserControl.InputBindings>
    <KeyBinding Modifiers="Ctrl" Key="Esc" Command="{Binding SaveCmd}"/>
</UserControl.InputBindings>

However the Command is never fired from the key-press, I think the issue(s) might be:

  • code above should not be working? (I found article where the bind should be done to the Command with DependencyProperty)
  • The key-press is caught by Visual Studio itself (CTRL + S is saving the file)
  • I might need to set the binding on the Window which encapsulates the UserControl
  • I might need to set the binding in the *Package.vsct and route it through as it would be a Command in Visual Studio

Question(s): How am I suppose to bind to the shortcut key-press? Where am I suppose to place the binding?

回答1:

You're right, the shortcut is used by a default Visual Studio command which takes precedence over the extension.

From a similar msdn post, this behavior is confirmed and the suggestion is to choose a different combination.

Find a reference for the complete list of VS shortcuts. Shortcut keys apply at various scopes (for example, when you are in a text editor, the Text Editor scoped shortcuts take precedence over the Global shortcuts). Aside from that, you can customize the shortcut's behavior and also import a new keyboard mapping scheme and select it under Tools > Options > Environment > Keyboard.

The KeyBindings section in .vsct is where you can associate a command with a keyboard shortcut. Microsoft sample is on github



回答2:

KeyBindings seems complicated and needs to be definied on several steps (also depends on requirements). This answer is written as a bonus to answer of user1892538.

Scenario: We got toolWindow which is already showing, but we want to add some command, which will invoke method in the view/view-model.


1. Create new Command (Step 3 in this tutorial):

Right-click to the project -> Add New Item -> Custom command. This will create 2 files and modify file with package:

  • CommandName.png - icon for the menu
  • CommandName.cs - class file including the source code of command
  • ProjectWindowPackage.cs - Package class with Initialize() method, which invokes Initialize() of CommandName.cs

MyWindowPackage.cs:

public sealed class MyWindowPackage : Package
{
    public const string PackageGuidString = "00000000-0000-0000-0000-000000000003";

    public MyWindowPackage() { }

    protected override void Initialize()
    {
        MyToolWindowCommand.Initialize(this);
        MySaveCommand.Initialize(this);
        base.Initialize();
    }
}

CommandName.cs:

// these 2 values will do the binding
public static readonly Guid ApplicationCommands
                                  = new Guid("00000000-0000-0000-0000-000000000002");
public const int SaveCommandId = 0x0201;

private readonly Package package;

private CommandName(Package package)
{
    // we need to have package (from Initialize() method) to set VS
    if (package == null) throw new ArgumentNullException("package");
    this.package = package;

    // this provides access for the Menu (so we can add our Command during init)
    OleMenuCommandService commandService = this.ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
    if (commandService != null)
    {
        // Creates new command "reference" (global ID)
        var menuCommandID = new CommandID(ApplicationCommands, SaveCommandId);
        // Create new command instance (method to invoke, ID of command)
        var menuItem = new MenuCommand(this.Save, menuCommandID);
        // Adding new command to the menu
        commandService.AddCommand(menuItem);
    }

    private void Save()
    {
        // Get instance of our window object (param false -> won't create new window)
        ToolWindowPane lToolWindow = this.package.FindToolWindow(typeof(MyToolWindow), 0, false);
        if ((null == lToolWindow) || (null == lToolWindow.Frame)) return;

        // Handle the toolWindow's content as Window (our control)
        ((lToolWindow as MyToolWindow)?.Content as MyWindowControl)?.Save();
    }
}

2. Set content of MyToolWindow to MyWindowControl (done when VSIX created):

MyToolWindow.cs:

[Guid("00000000-0000-0000-0000-000000000001")] //GUID of ToolWindow
public class MyToolWindow : ToolWindowPane
{
    public MyToolWindow() : base(null)
    {
        this.Content = new MyWindowControl();
    }
}

3. Set code in code-behind to invoke ViewModel (or do the job itself):

MyWindowControl.cs:

public partial class MyWindowControl : UserControl
{
    // output omitted for brevity

    public void Save()
    {
        // Do the call towards view-model or run the code

        (this.DataContext as MyViewModel)?.SaveCmd.Execute(null);
    }
}

4. Set Command with Shortcut so VS know how to handle them:

In MZTools' article can be found solution how to add Command without seeing it in menu, but if You will go to Tools->Window->Keyboard, You might find them there (so You can set up shortcut).

I will show both origin Button (for displaying tool window) and 2nd invisible Button used for the shortcut (Keybind) only.

MyWindowPackage.vsct (in several parts):

<!-- shows the definition of commands/buttons in menu, Canonical name is how You can find the command in VS [Tools -> Keyboard -> CommandName] -->
<Commands package="guidMyWindowPackage">

    <Button guid="guidMyWindowPackageCmdSet"
            id="MyWindowCommandId"
            priority="0x0100"
            type="Button">
    <Parent guid="guidSHLMainMenu" id="IDG_VS_WNDO_OTRWNDWS1" />
    <Strings>
      <ButtonText>My ToolWindow</ButtonText>
      <CommandName>MyCommand</CommandName>
      <MenuText>My ToolWindow</MenuText>
      <LocCanonicalName>MyToolWindow</LocCanonicalName>
    </Strings>
  </Button>

  <Button guid="guidMyWindowPackageCmdSet"
          id="MySaveCommandId"
          priority="0x0100"
          type="Button">
    <Strings>
      <ButtonText>My ToolWindow Save</ButtonText>
      <LocCanonicalName>MyToolWindow.Save</LocCanonicalName>
    </Strings>
  </Button>
</Buttons>
</Commands>

KeyBindings (shortcut definition):

<KeyBindings>
    <KeyBinding guid="guidMyWindowPackageCmdSet"
                id="MySaveCommandId"
                editor="guidVSStd97"
                key1="1" mod1="Control" />
</KeyBindings>

And the Symbols, which set and bind GUID, Command definition and Command logic together:

<Symbols>
    <!-- This is the package guid. -->
    <GuidSymbol name="guidMyWindowPackage" value="{00000000-0000-0000-0000-000000000003}" />

    <!-- This is the guid used to group the menu commands together -->
    <GuidSymbol name="guidMyWindowPackageCmdSet" value="{00000000-0000-0000-0000-000000000002}">
        <IDSymbol name="MyWindowCommandId" value="0x0100" />
        <IDSymbol name="MySaveCommandId" value="0x0201" />
    </GuidSymbol>

<!-- some more GuidSymbols -->

</Symbols>

Bonus:

KeyBinding does have property editor="guidVSStd97", this will set the scope of binding to "GENERAL" (useable in each window). If You can set this to the GUID of Your ToolWindow, it will be used only when the ToolWindow is selected. How it works, is described behind this link. And to acomplish it full, get to this link.



回答3:

Good answer from user1892538

Also, beware that some shortcuts are taken by the operating system or by other software running on the machine.

Ctrl+Esc will activate the start menu on Windows machines.

Other examples: - Intel graphics software takes over Ctrl+Alt+F8. - Some Ctrl+Alt combinations can have trouble getting through remote desktop connections.