Skip to content

Command Registry & UI Binding

The Command Registry acts as the single, read-only source of truth for all user actions. Hand-coding action-based UI elements (such as context menus and menu bars) in XAML is prohibited.

Each command registered in the system MUST define:

  • System Name: The internal functional identifier (e.g., App.CloseCurrentDocument).
  • Valid Views: The specific /Views where this command is allowed to execute.
  • Locations: Hierarchical dot-notation strings defining where the command logically appears in the UI (e.g., menu.books.view).
  • Intents: A logical identifier representing the user's objective (e.g., exit_to_library), decoupled from the underlying system action.

Dynamic UI Generation

  • The UI MUST query the Command Registry to generate dynamic layouts (e.g., Menus, Command Palettes).
  • Subset Grouping: The Registry SHALL sort and group commands by their Location string. When dynamically yielding UI lists, the Registry MUST automatically inject a visual separator (e.g., <Separator>) whenever the subset identifier changes (e.g., transitioning from menu.books.view to menu.books.management).

Shortcut Routing

  • Shortcuts are contextual and hierarchical. User-defined shortcuts are stored externally and applied at runtime.
  • The Registry MUST intercept global key presses and evaluate them against the active View. If a shortcut is valid in a global context (e.g., Bookshelf), it remains valid in child views unless explicitly overridden by a more specific command mapped to that View.

Code Example: Dynamic Menu

Instead of hardcoding <MenuItem>, the UI binds to lists provided by the Command Registry. Using DataTemplates, Avalonia automatically knows whether to draw a clickable button or a dividing line based on the object type.

<Menu ItemsSource="{Binding CommandRegistry.Menu_Books}">
    <Menu.DataTemplates>

        <!-- 1. If the item is a standard Command -->
        <DataTemplate DataType="{x:Type models:RegisteredCommand}">
            <!-- It automatically fetches the translated text using the Intent -->
            <MenuItem Header="{i18n:Localize {Binding Intent}}" 
                      Command="{Binding SystemAction}" 
                      InputGesture="{Binding ActiveShortcut}" />
        </DataTemplate>

        <!-- 2. If the Registry injected a Separator because the 'Location' subset changed -->
        <DataTemplate DataType="{x:Type models:MenuSeparator}">
            <Separator />
        </DataTemplate>

    </Menu.DataTemplates>
</Menu>

Defining the Menu Shapes (The Topology)

If the Command Registry is just a giant list of commands, the doesn't UI actually know that the "Books" menu comes before "Edit", or that the "Lifecycle" section comes before "Management".

Because the Command Registry only provides the buttons, I need a separate schema (a JSON file or a C# configuration class) to define the "empty buckets" to define the UI Topology.

It looks something like this:

[
  { 
    "id": "menu.books", 
    "order": 30,
    "sections": [
      { "id": "lifecycle", "order": 10 },
      { "id": "view", "order": 20 }
    ] 
  }
]

How it comes together:

  1. Draw the buckets: The UI reads this schema. It draws the top-level menus from left to right. (Using an order number is trick so I can easily squeeze a new menu right between 20 and 30 later without having to rearrange a massive hardcoded array).
  2. Fill the buckets: The UI asks the Command Registry, "Hey, give me all the commands tagged with menu.books.lifecycle", and drops them into the corresponding empty space.

This keeps "what the menu looks like" totally separate from "what buttons are currently allowed to be clicked."