Inhaltsverzeichnis
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 Übersicht.
Voraussetzungen
- Grundverständnis des Manifests und der Command-Deklaration
- Ein lauffähiges Add-in mit mindestens einem deklarierten Command (z. B. das 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 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 <a href="command:wis.proofExec.open"> → 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
TLazyCommandHandlerist: Stub entfernt sich, DLL wird geladen, ABI-Handshake,CreatePlugin,Activate— das Add-in registriert den echten Handler. - Echter Handler —
ICommandHandler.Executewird 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 Tutorial oder zurück zur Übersicht.