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
/Viewswhere 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
Locationstring. When dynamically yielding UI lists, the Registry MUST automatically inject a visual separator (e.g.,<Separator>) whenever the subset identifier changes (e.g., transitioning frommenu.books.viewtomenu.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:
- Draw the buckets: The UI reads this schema. It draws the top-level menus from left to right. (Using an
ordernumber is trick so I can easily squeeze a new menu right between 20 and 30 later without having to rearrange a massive hardcoded array). - 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."