Tutorial: Document-View Add-in

Dieses Tutorial erweitert das Hello World Add-in um eine Dokumentansicht. Das Add-in registriert einen Dokumenttyp mit einer Factory und stellt einen TWvdSDocumentFrame bereit, den der Host als Tab in die Shell einbettet.

Zurück zur Übersicht.

Was gebaut wird

Ein Add-in namens „Asset Editor“, das:

  • einen Command „Anlage öffnen“ registriert
  • eine IWvdSDocumentFactory bereitstellt
  • bei Aufruf des Commands einen Dokument-Tab öffnet
  • Dirty-State, Titel und Speichern unterstützt

Manifest

{
  "id": "demo.asset-editor",
  "name": "asset-editor",
  "displayName": "Asset Editor",
  "version": "1.0.0",
  "publisher": "demo",
  "kind": "native",
  "main": "bin/AssetEditor.dll",
  "engineVersion": "^1.0.0",
 
  "activationEvents": [
    "onCommand:assets.open"
  ],
 
  "contributes": {
    "commands": [
      {
        "command": "assets.open",
        "title": "Anlage öffnen",
        "category": "Assets",
        "icon": "media/asset-open.svg"
      },
      {
        "command": "assets.save",
        "title": "Anlage speichern",
        "category": "Assets"
      }
    ],
    "menus": {
      "menuBar/file": [
        { "command": "assets.open", "group": "open", "order": 10 }
      ]
    },
    "toolbar": [
      {
        "toolbarId": "actionbar",
        "command": "assets.save",
        "icon": "media/save.svg",
        "tooltip": "Anlage speichern",
        "when": "activeDocument == assets.editor && isDirty"
      }
    ],
    "keybindings": [
      {
        "command": "assets.save",
        "key": "Ctrl+S",
        "when": "activeDocument == assets.editor"
      }
    ]
  }
}

Hier fallen mehrere Dinge zusammen: Der Save-Command ist nur in der ActionBar sichtbar, wenn ein Asset-Editor aktiv ist und ungespeicherte Änderungen vorliegen. Das Tastenkürzel Ctrl+S ist ebenfalls kontextgebunden. Dadurch stört das Add-in nicht, wenn ein anderer Dokumenttyp aktiv ist.

Document Factory

Die Factory erzeugt auf Anfrage einen neuen Frame und teilt dem Host den Dokumenttyp mit.

unit AssetEditorFactory;
 
{$mode objfpc}{$H+}
 
interface
 
uses
  Classes,
  WvdS.Document.Host.Api,
  AssetEditorFrame;
 
type
  TAssetEditorFactory = class(TInterfacedObject, IWvdSDocumentFactory)
  private
    FHost: IHost;
  public
    constructor Create(const AHost: IHost);
    function CreateFrame(AOwner: TComponent): TWvdSDocumentFrame;
    function GetDocumentType: string;
    function GetDocumentKind: TDocumentKind;
    function GetDisplayName: string;
    function GetIcon: string;
  end;
 
implementation
 
constructor TAssetEditorFactory.Create(const AHost: IHost);
begin
  inherited Create;
  FHost := AHost;
end;
 
function TAssetEditorFactory.CreateFrame(AOwner: TComponent): TWvdSDocumentFrame;
var
  Frame: TAssetEditorFrame;
begin
  Frame := TAssetEditorFrame.Create(AOwner);
  Frame.Initialize(FHost);
  Result := Frame;
end;
 
function TAssetEditorFactory.GetDocumentType: string;
begin
  Result := 'assets.editor';
end;
 
function TAssetEditorFactory.GetDocumentKind: TDocumentKind;
begin
  Result := dkMultiInstance;
end;
 
function TAssetEditorFactory.GetDisplayName: string;
begin
  Result := 'Asset Editor';
end;
 
function TAssetEditorFactory.GetIcon: string;
begin
  Result := 'media/asset-open.svg';
end;
 
end.

dkMultiInstance erlaubt mehrere Tabs desselben Typs. Wenn nur ein einziger Tab erlaubt sein soll (etwa für eine Übersichtsliste), wäre dkSingleInstance die richtige Wahl.

Document Frame

Der Frame ist ein gewöhnlicher LCL-TFrame, der von TWvdSDocumentFrame erbt. Er enthält die eigentliche Benutzeroberfläche.

unit AssetEditorFrame;
 
{$mode objfpc}{$H+}
 
interface
 
uses
  Classes, SysUtils, Controls, Forms, StdCtrls,
  WvdS.Document.Host.Api;
 
type
  TAssetEditorFrame = class(TWvdSDocumentFrame)
  private
    FHost: IHost;
    FContext: IDocumentContext;
    FNameEdit: TEdit;
    FDescMemo: TMemo;
    procedure HandleChange(Sender: TObject);
  protected
    function DoCanClose: Boolean; override;
    procedure DoSave; override;
    procedure DoActivate; override;
    procedure DoDeactivate; override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure Initialize(const AHost: IHost);
    procedure SetDocumentContext(const AContext: IDocumentContext);
  end;
 
implementation
 
uses
  Dialogs;
 
constructor TAssetEditorFrame.Create(AOwner: TComponent);
var
  Lbl: TLabel;
begin
  inherited;
 
  Lbl := TLabel.Create(Self);
  Lbl.Parent := Self;
  Lbl.Caption := 'Bezeichnung:';
  Lbl.Left := 12;
  Lbl.Top := 12;
 
  FNameEdit := TEdit.Create(Self);
  FNameEdit.Parent := Self;
  FNameEdit.Left := 12;
  FNameEdit.Top := 32;
  FNameEdit.Width := 300;
  FNameEdit.OnChange := @HandleChange;
 
  Lbl := TLabel.Create(Self);
  Lbl.Parent := Self;
  Lbl.Caption := 'Beschreibung:';
  Lbl.Left := 12;
  Lbl.Top := 64;
 
  FDescMemo := TMemo.Create(Self);
  FDescMemo.Parent := Self;
  FDescMemo.Left := 12;
  FDescMemo.Top := 84;
  FDescMemo.Width := 300;
  FDescMemo.Height := 200;
  FDescMemo.OnChange := @HandleChange;
end;
 
procedure TAssetEditorFrame.Initialize(const AHost: IHost);
begin
  FHost := AHost;
end;
 
procedure TAssetEditorFrame.SetDocumentContext(const AContext: IDocumentContext);
begin
  FContext := AContext;
  FContext.SetTitle('Neue Anlage');
end;
 
procedure TAssetEditorFrame.HandleChange(Sender: TObject);
begin
  if Assigned(FContext) then
  begin
    FContext.SetDirty(True);
    FHost.Context.SetContext('isDirty', 'true');
  end;
end;
 
function TAssetEditorFrame.DoCanClose: Boolean;
begin
  if Assigned(FContext) and FContext.GetDirty then
  begin
    case MessageDlg('Änderungen speichern?',
      mtConfirmation, [mbYes, mbNo, mbCancel], 0) of
      mrYes:
      begin
        DoSave;
        Result := True;
      end;
      mrNo:
        Result := True;
    else
      Result := False;
    end;
  end
  else
    Result := True;
end;
 
procedure TAssetEditorFrame.DoSave;
begin
  // Hier: Daten speichern (Datei, Datenbank, API)
  FHost.Notifications.ShowInfo('Anlage gespeichert: ' + FNameEdit.Text);
 
  if Assigned(FContext) then
    FContext.SetDirty(False);
  FHost.Context.SetContext('isDirty', '');
end;
 
procedure TAssetEditorFrame.DoActivate;
begin
  FHost.Context.SetContext('activeDocument', 'assets.editor');
  if Assigned(FContext) and FContext.GetDirty then
    FHost.Context.SetContext('isDirty', 'true')
  else
    FHost.Context.SetContext('isDirty', '');
end;
 
procedure TAssetEditorFrame.DoDeactivate;
begin
  FHost.Context.SetContext('activeDocument', '');
  FHost.Context.SetContext('isDirty', '');
end;
 
end.

Die vier virtuellen Methoden bilden den Vertrag zwischen Frame und Host:

  • DoCanClose — Der Host fragt vor dem Schließen. Gibt der Frame False zurück, bleibt der Tab offen.
  • DoSave — Der Host ruft diese Methode bei Ctrl+S auf. Der Frame speichert und setzt Dirty := False.
  • DoActivate — Der Host ruft diese Methode beim Tab-Wechsel auf. Der Frame setzt Context Keys, damit die ActionBar und die Menüs korrekt reagieren.
  • DoDeactivate — Der Host ruft diese Methode auf, wenn ein anderer Tab aktiviert wird. Der Frame räumt seine Context Keys auf.

Plugin-Klasse

unit AssetEditorPlugin;
 
{$mode objfpc}{$H+}
 
interface
 
uses
  WvdS.Document.Host.Api;
 
type
  TAssetEditorPlugin = class(TInterfacedObject, IPlugin)
  private
    FHost: IHost;
  public
    constructor Create(const AHost: IHost);
    procedure Activate(const AContext: IExtensionContext);
    procedure Deactivate;
  end;
 
implementation
 
uses
  AssetEditorFactory;
 
type
  TOpenAssetHandler = class(TInterfacedObject, ICommandHandler)
  private
    FHost: IHost;
  public
    constructor Create(const AHost: IHost);
    procedure Execute;
  end;
 
  TSaveAssetHandler = class(TInterfacedObject, ICommandHandler)
  private
    FHost: IHost;
  public
    constructor Create(const AHost: IHost);
    procedure Execute;
  end;
 
{ TAssetEditorPlugin }
 
constructor TAssetEditorPlugin.Create(const AHost: IHost);
begin
  inherited Create;
  FHost := AHost;
end;
 
procedure TAssetEditorPlugin.Activate(const AContext: IExtensionContext);
begin
  // Document Factory registrieren
  AContext.Subscribe(
    FHost.Documents.RegisterFactory('assets.editor',
      TAssetEditorFactory.Create(FHost))
  );
 
  // Command-Handler registrieren
  AContext.Subscribe(
    FHost.Commands.RegisterCommand('assets.open', 'Anlage öffnen',
      TOpenAssetHandler.Create(FHost))
  );
 
  AContext.Subscribe(
    FHost.Commands.RegisterCommand('assets.save', 'Anlage speichern',
      TSaveAssetHandler.Create(FHost))
  );
end;
 
procedure TAssetEditorPlugin.Deactivate;
begin
end;
 
{ TOpenAssetHandler }
 
constructor TOpenAssetHandler.Create(const AHost: IHost);
begin
  inherited Create;
  FHost := AHost;
end;
 
procedure TOpenAssetHandler.Execute;
begin
  FHost.Documents.OpenDocument('assets.editor');
end;
 
{ TSaveAssetHandler }
 
constructor TSaveAssetHandler.Create(const AHost: IHost);
begin
  inherited Create;
  FHost := AHost;
end;
 
procedure TSaveAssetHandler.Execute;
begin
  // Der Host leitet den Save-Command an den aktiven Frame weiter
end;
 
end.

In Activate werden drei Dinge registriert: die Document Factory und zwei Command-Handler. Alle drei Registrierungen geben ein IDisposable zurück, das an AContext.Subscribe übergeben wird. Beim Deaktivieren des Add-ins räumt der Host alles automatisch auf.

Zusammenspiel

Der Ablauf beim Öffnen eines Dokuments:

  1. Benutzer wählt „Anlage öffnen“ im Menü.
  2. Host ruft TOpenAssetHandler.Execute auf.
  3. Handler ruft FHost.Documents.OpenDocument('assets.editor') auf.
  4. Host fragt TAssetEditorFactory.GetDocumentKind ab → dkMultiInstance.
  5. Host ruft TAssetEditorFactory.CreateFrame(TabContainer) auf.
  6. Factory erzeugt TAssetEditorFrame und gibt ihn zurück.
  7. Host bettet den Frame in einen neuen Tab ein.
  8. Host ruft SetDocumentContext auf. Der Frame setzt den Titel.
  9. Host ruft DoActivate auf. Der Frame setzt Context Keys.
  10. Die ActionBar zeigt den Save-Button (wegen when: activeDocument == assets.editor).

Weiter zum Sidebar-View Tutorial oder zurück zur Übersicht.

Zuletzt geändert: den 15.03.2026 um 02:32