====== Tutorial: Command-Routing über den ShellCommandRouter ======
Ziel dieses Tutorials ist es, das zentrale Dispatch-Muster der WvdS Add-in Host Shell zu verstehen und korrekt anzuwenden. Im Unterschied zu klassischen Plugin-Architekturen, in denen der Host Fenster oder Views direkt erzeugt, delegiert die Shell **jede** Benutzeraktion über eine einzige Stelle: den ''ShellCommandRouter''. Der Vorteil besteht darin, dass Builtin-Handler, WelcomePage-Links, Toolbar-Buttons und Tastenkürzel denselben Code-Pfad durchlaufen — Lazy-Loading, Aktivierung und Fehlerbehandlung greifen automatisch, ohne dass der Aufrufer etwas davon wissen muss.
Zurück zur [[..:start|Übersicht]].
===== Voraussetzungen =====
* Grundverständnis des [[..:manifest|Manifests]] und der [[..:contributions:commands|Command-Deklaration]]
* Ein lauffähiges Add-in mit mindestens einem deklarierten Command (z. B. das [[hello-world|Hello World Add-in]])
===== Das zentrale Prinzip =====
Die Shell besitzt einen einzigen Dispatch-Punkt für alle Command-Aufrufe: den ''ShellCommandRouter''. Jeder Auslöser — ob Menüklick, Toolbar-Button, Tastenkürzel, WelcomePage-Link oder programmatischer Aufruf — landet am selben Router. Von dort übernimmt der Host:
- Prüfung, ob ein Handler registriert ist
- Lazy-Stub-Auflösung (DLL laden, ABI-Handshake, Activate)
- Aufruf des echten Handlers
- Fehlerbehandlung und Logging
Kein Aufrufer muss wissen, ob der Ziel-Command nativ, per Script oder noch gar nicht geladen ist.
===== Schritt 1: Command im Manifest deklarieren =====
Jeder Command, der über den Router erreichbar sein soll, muss im ''plugin.json'' stehen. Die Deklaration allein reicht aus, damit der Host einen Lazy-Stub registriert und den Command in der gesamten UI sichtbar macht.
{
"contributes": {
"commands": [
{
"command": "wis.proofExec.open",
"title": "%cmdProofExecTitle%",
"category": "WIS",
"icon": "media/checklist.svg"
}
]
}
}
Ab diesem Zeitpunkt erscheint der Command in der CommandPalette, kann in Menüs, Toolbar und Keybindings referenziert werden — obwohl noch keine DLL geladen ist.
===== Schritt 2: Handler im Add-in registrieren =====
In der ''Activate''-Methode registriert das Add-in den echten Handler. Der Host ersetzt damit den Lazy-Stub.
type
TOpenProofExecHandler = class(TInterfacedObject, ICommandHandler)
private
FHost: IHost;
public
constructor Create(const AHost: IHost);
procedure Execute;
end;
procedure TOpenProofExecHandler.Execute;
begin
FHost.Documents.OpenDocument('wis.proofExec');
end;
procedure TWISPlugin.Activate(const AContext: IExtensionContext);
begin
AContext.Subscribe(
FHost.Commands.RegisterCommand('wis.proofExec.open',
'Prüfausführung öffnen',
TOpenProofExecHandler.Create(FHost))
);
end;
Der Handler ruft ''FHost.Documents.OpenDocument'' auf — er erzeugt **nicht** selbst einen Frame oder ein Fenster. Das ist Aufgabe der registrierten ''IWvdSDocumentFactory'' (siehe [[document-view|Document-View Tutorial]]).
===== Schritt 3: Programmatisch einen Plugin-Command auslösen =====
Wenn Shell-eigener Code oder ein anderes Add-in den Command auslösen will, genügt ein einziger Aufruf:
FHost.Commands.ExecuteCommand('wis.proofExec.open');
Dieser Aufruf funktioniert wie ein Benutzerklick: Er löst die Aktivierung des Ziel-Add-ins aus, falls es noch nicht geladen ist, und führt dann den Handler aus. Das ist der korrekte Weg, wie Builtin-Handler an Plugin-Commands delegieren.
===== Schritt 4: WelcomePage-Links nutzen denselben Pfad =====
Links auf der WelcomePage verwenden das ''command:''-Schema. Beim Klick durchläuft der Link denselben Router:
Klick auf
→ OnHotClick
→ Validierung des command:-Schemas
→ ShellCommandRouter.ExecuteCommand()
→ Lazy-Stub oder echter Handler
Es gibt keinen separaten Code-Pfad für WelcomePage-Links — sie sind gleichberechtigte Command-Auslöser.
===== Das Anti-Pattern: Views direkt erzeugen =====
Das folgende Muster ist **falsch** und darf nicht verwendet werden:
// FALSCH: Builtin-Handler erzeugt Plugin-View direkt
procedure TShellBuiltinHandler.Execute;
var
LFrame: TWISProofExecFrame;
begin
LFrame := TWISProofExecFrame.Create(FTabContainer);
LFrame.Parent := FTabContainer;
LFrame.Initialize(FHost);
end;
Drei Probleme entstehen:
* **Kopplung:** Die Shell kennt Plugin-Klassen und muss sie importieren. Änderungen am Plugin erfordern einen Shell-Rebuild.
* **Kein Lazy-Loading:** Die Plugin-DLL muss beim Start geladen sein, nicht erst bei Bedarf.
* **Kein Lifecycle:** Der Host weiß nichts vom erzeugten Frame — kein Tab-Management, kein Dirty-State, kein ''DoCanClose'', kein Context-Wechsel.
===== Was unter der Haube passiert =====
Der vollständige Ablauf bei einem Command-Aufruf über den Router:
- **Auslöser** — Menü, Toolbar, Keybinding, WelcomePage-Link oder ''ExecuteCommand''.
- **ShellCommandRouter** — Empfängt die Command-ID, sucht den Handler in der Registry.
- **Lazy-Stub?** — Falls der Handler ein ''TLazyCommandHandler'' ist: Stub entfernt sich, DLL wird geladen, ABI-Handshake, ''CreatePlugin'', ''Activate'' — das Add-in registriert den echten Handler.
- **Echter Handler** — ''ICommandHandler.Execute'' wird aufgerufen.
- **Fehler?** — Exception wird vom Host gefangen, geloggt und dem Benutzer gemeldet. Nach drei Fehlern wird das Add-in deaktiviert.
Dieser Ablauf ist identisch für alle Auslöser. Es gibt keine Sonderwege.
===== Zusammenfassung =====
| Richtig | Falsch |
| ''FHost.Commands.ExecuteCommand('plugin.command')'' | Plugin-Klassen direkt instanziieren |
| Command-ID als einzige Schnittstelle | Plugin-DLL im Shell-Code importieren |
| Host übernimmt Lifecycle automatisch | Frame manuell in Container einbetten |
| Lazy-Loading funktioniert transparent | DLL muss beim Start geladen sein |
Das Routing über den ''ShellCommandRouter'' ist kein optionales Pattern, sondern die verbindliche Architektur. Jeder Builtin-Handler, jeder WelcomePage-Link und jeder programmatische Aufruf muss diesen Weg nehmen.
Weiter zum [[script-command|Script-Command Tutorial]] oder zurück zur [[..:start|Übersicht]].