====== 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]].