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

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:

  1. Prüfung, ob ein Handler registriert ist
  2. Lazy-Stub-Auflösung (DLL laden, ABI-Handshake, Activate)
  3. Aufruf des echten Handlers
  4. 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.

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:

  1. Auslöser — Menü, Toolbar, Keybinding, WelcomePage-Link oder ExecuteCommand.
  2. ShellCommandRouter — Empfängt die Command-ID, sucht den Handler in der Registry.
  3. 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.
  4. Echter HandlerICommandHandler.Execute wird aufgerufen.
  5. 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.

Zuletzt geändert: den 18.03.2026 um 22:09