tgjones/gemini — pull request #79 by tgjones (2014-11-10 13:35:52+00:00)
Title: Introduce command system.

Gemini now has a unified command system. Commands are defined once, and can then be added to menus and toolbars. Command handlers are defined separately from the commands themselves, to allow alternative command handler selection; for example, the currently active document might get a chance to handle the command first (not yet implemented). It will now be possible to completely customise the built-in menu and toolbar items, much more easily than previously.

It's not too late for discussion and changes to the design, so I'm opening this pull request while I continue working on this feature. If anyone has any feedback about commands in Gemini, please add it here. Eventually, the design notes here should be added to the documentation.

This is most definitely a breaking change; code that explicitly adds menu or toolbar items will need to be changed to use this new command system.

## How commands work

I reviewed how Visual Studio, MonoDevelop and SharpDevelop implement their respective command systems. Unsurprisingly, Visual Studio has the most flexible command system, but also the most complicated. As far as I know, MonoDevelop's command system evolved from SharpDevelop's command system, but MonoDevelop has added some nice things on top. All 3 IDEs use XML for configuration.

Out of those 3, the system I've designed for Gemini is most similar to MonoDevelop, but unlike MonoDevelop, all the configuration is done in C#. Here are the main moving parts:

### Command definitions

It all starts with a command definition. Here's the command definition for the built-in "Undo" command:

``` csharp
[Export(typeof(CommandDefinition))]
public class UndoCommandDefinition : CommandDefinition
{
public const string CommandName = "Edit.Undo";

public override string Name
{
get { return CommandName; }
}

public override string Text
{
get { return "_Undo"; }
}

public override string ToolTip
{
get { return "Undo"; }
}

public override Uri IconSource
{
get { return new Uri("pack://application:,,,/Gemini;component/Resources/Icons/Undo.png"); }
}

public override KeyGesture KeyGesture
{
get { return new KeyGesture(Key.Z, ModifierKeys.Control); }
}
}
```

Note that there's nothing about how to handle the command. Command *definitions* and command *handlers* are separated. That's very useful. I haven't yet implemented it, but I plan to make it possible to inject your own command handlers for any command, including built-in commands, in your own documents or tool windows, and whatever has the focus gets the first chance to handle the command.

### Command handlers

Here's the command handler for the "Undo" command:

``` csharp
[CommandHandler(typeof(UndoCommandDefinition))]
public class UndoCommandHandler : CommandHandler
{
[Import]
private IShell _shell;

public override void Update(Command command)
{
command.Enabled = (_shell.ActiveItem != null && _shell.ActiveItem.UndoRedoManager.UndoStack.Any());
}

public override Task Run(Command command)
{
_shell.ActiveItem.UndoRedoManager.Undo(1);
return TaskUtility.Completed;
}
}
```

`Update` gives the command handler a chance to change properties on the command - `Enabled`, `Text`, `Visible`, etc. - before showing any menu item or toolbar button using this command.

`Run`, as it sounds like, executes the command. Note that `Run` returns a `Task` - I want to move towards the `async`/`await` style of programming, and away from Caliburn Micro's coroutines.

I'm probably going to remove the `[CommandHandler]` attribute from here, and instead add a property to `CommandDefinition` to let you define the global handler. The global handler will be used if no other handlers have been found in the view model hierarchy.

### Menus and Toolbars

To use a command on a menu or toolbar, you need to associate it with a menu item or toolbar button. Here is the "Undo" menu item:

``` csharp
[Export]
public static MenuItemDefinition EditUndoMenuItem = new CommandMenuItemDefinition(
MainMenu.MenuDefinitions.EditUndoRedoMenuGroup, 0); // Parameters are parent group, and sort order
```

I haven't yet implemented toolbar buttons, but they'll follow the same pattern.

### Overriding menu and toolbar construction

Menus are built using the `IMenuBuilder` interface:

``` csharp
public interface IMenuBuilder
{
void BuildMenuBar(MenuBarDefinition menuBarDefinition, MenuModel result);
}
```

There's a default `MenuBuilder`, which `[Import]`s all the menu definitions, menu item group definitions, and menu item definitions, and constructs the menu for a given menu bar (although in most cases there will only be one menu bar, the main menu).

You can also implement your own menu builder, if you prefer to start from scratch and add the items you want, structured however you want. (I've been wanting to support this for a while.)

## To do

* [ ] Implement list-type commands (can generate dynamic menu items, such as "Recent documents" or "Active windows")
* [ ] Implement radio and check commands
* [ ] Add Command tool

This implements #63.

cc @4ux-nbIx