Details zum Action & Command Framework

Technischer Überblick

Mit den Informationen aus „Action & Command Framework“ als Grundlage, soll an dieser Stelle ein technischer Überblick zu den beteiligten Komponenten des Action & Command Frameworks gegeben werden. Der Zweck jeder Komponente wird verdeutlicht und ihr Zusammenspiel wird erläutert.

Um in einer grafischen Benutzeroberfläche ausführbare Funktionen anzubieten, sind drei Teilaspekte zu betrachten:

  1. Repräsentation der Funktionalität. Schaltflächen in einer GUI sind hierfür ein typisches Beispiel. Die Repräsentation kann dabei Dinge wie den Text der Schaltfläche oder das abgebildete Icon umfassen. Auch ein Tooltip-Text fällt unter diesen Aspekt. Die Anordnung mehrerer Elemente innerhalb der Gesamtoberfläche zählt jedoch nicht dazu – diese Fragestellung wird beispielsweise durch das Layouting oder die Zusammenstellung von Symbolleisten beantwortet. Die Repräsentation einer Funktionalität in einer GUI wird als Aktion bezeichnet.

  2. Kapselung der ausführbaren Funktionalität. Dieser Aspekt definiert, was geschehen soll wenn der Nutzer die Aktion auslöst. Das Objekt, welches die ausführbare Funktionalität kapselt, wird als Kommando bezeichnet.

  3. Bereitstellen von Statusinformationen für das Kommando und die Aktion. Da die ausführbare Funktionalität davon abhängt, welchen Zustand die Gesamtanwendung hat, muss sie diesen Zustand kennen. Meistens wird er innerhalb von Domänenobjekten gespeichert; in manchen Fällen ist auch der Zugriff auf andere Bestandteile der GUI notwendig. Eine Funktionalität zum Blättern auf die nächste Seite kann beispielsweise nur dann durchgeführt werden, wenn eine solche Seite existiert.

    Gleiches gilt für die Aktion, die eine ausführbare Funktionalität repräsentiert: In vielen Fällen wird die Darstellung dem Programmzustand angepasst. Falls im Beispiel das Blättern auf die nächste Seite nicht möglich ist, würde ein Button, der diese Funktionalität repräsentiert, inaktiv dargestellt werden.

    Allgemein gefasst stellt dieser Aspekt die Verbindung zwischen den aktuell im Programmablauf existierenden Objekten, sowie den Kommandos und Aktionen her. Die Objekte bilden den Kontext, in dem das Kommando ausgeführt wird. Deshalb wird dieser Teilaspekt auch als Kontext bezeichnet.

Prinzipiell könnten die drei Teilaspekte innerhalb einer einzigen Klasse umgesetzt werden. Um die Codebasis flexibel und wartbar zu halten, ist es jedoch wichtig, die einzelnen Aufgaben voneinander zu trennen. Dies bringt verschiedene Vorteile:

  • Durch die Trennung von Aktion und Kommando entsteht eine flexible m:n-Abbildung. Dasselbe Kommando kann durch verschiedene Aktionen an unterschiedlichen Stellen der GUI ausgelöst werden. Genauso kann eine Aktion bei Bedarf mehrere Kommandos ausführen.

  • Durch die Trennung des Kommandos von seinen Kontextobjekten wird es vielseitig einsetzbar. In einer Anwendung, die zwei Dokumente nebeneinander darstellt, könnte das beispielhaft erwähnte Kommando zum Blättern auf die nächste Seite ohne Veränderung zwei Mal eingesetzt werden. Über den jeweiligen Kontext erhält jedes Kommando Zugriff auf das ihm zugeordnete Dokument. Auch die Erweiterung auf mehr als zwei Dokumente ist leicht möglich.

Die Trennung der drei Teilaspekte führt dazu, dass Aktionen und Kommandos relativ leicht implementierbar sind. Im Regelfall entstehen gut überschaubare Klassen, die auf vielfältige Weise kombiniert werden können und voneinander unabhängig sind. Oft bietet es sich an, für Integrationen eigene Kommando- und Aktions-Implementationen zu erstellen.

Beim Kontext sind im Gegensatz dazu keine speziellen Implementationen vorgesehen. Stattdessen muss betrachtet werden, welche Objekte in einem Kontext abgelegt werden sollen damit die eingesetzten Kommandos alle Statusinformationen erhalten, die sie zur Ausführung benötigen. In komplexeren Szenarien kann es daher notwendig sein, mehrere Kontexte aufzubauen und diese unter Umständen durch eine Hierarchie miteinander zu verbinden. Der folgende Abschnitt geht auf diese Themen näher ein. Im Anschluss daran finden sich Detailinformationen zu Kommandos und Aktionen.

Der Kontext (Context)

Instanzen der Klasse Context bilden die Brücke zwischen GUI-Elementen, Aktionen und Kommandos.

Kommandos benötigen zu ihrer Ausführung eine Anzahl von Objekten, Argumente genannt, die den Zustand der Benutzeroberfläche zum Zeitpunkt der Ausführung der Aktion widerspiegeln. Zu dieser Anzahl von Objekten kann jedes GUI-Element eigene Objekte beisteuern, sodass letztlich eine Ansammlung von Objekten entsteht, auf die sich aus Sicht des Benutzers ein Kommando bezieht. Diese Ansammlung wird Kontext genannt.

Im Besonderen können auch GUI Elemente als semantische Einheiten verstanden und der Sammlung hinzugefügt werden. Ein Beispiel: Ein Fenster beinhaltet eine Viewerinstanz, eine Menüleiste und eine Werkzeugleiste. Jede Aktion, die innerhalb dieses Fensters passiert, kann Auswirkungen auf den Zustand (enabled state), aber auch auf die Art der Ausführung der Werkzeuge in der Werkzeugleiste oder der Menüeinträge in der Menüleiste haben. Ebenso können aber auch die Werkzeuge oder Menüeinträge mit Objekten des Fensters arbeiten. Damit bildet das Fenster in sich eine logische Einheit, dessen einzelne Elemente in ihrem Zustand und ihrer Ausführbarkeit voneinander abhängen.

Kontext-Hierarchie

In komplexeren Anwendungen ist es oft sinnvoll, mehr als einen Kontext zu nutzen. Diese können entweder unabhängig voneinander sein oder Zugriff auf die Elemente anderer Kontexte haben. Wie die Sichtbarkeit festgelegt werden kann, wird weiter unten erläutert.

Instanzen von Context sind stets einer GUI-Komponente zugeordnet. Da die grafische Benutzeroberfläche eine Objekthierarchie bildet, unterstützen auch Kontexte die hierarchische Anordnung. Soll der im Beispiel beschriebenen Oberfläche ein weiterer Viewer hinzugefügt werden, der eine eigene Werkzeugleiste mitbringt und von den anderen Komponenten unabhängig ist, kann zu diesem Zweck ein weiterer, unabhängiger Kontext angelegt werden, der lediglich dem zweiten Viewer und seiner Werkzeugleiste zugeordnet wird.

Es ist nicht zwingend notwendig, jedes GUI-Element mit einem eigenen Kontext zu versehen. Vielmehr wird zusammengehörigen Elementen ein gemeinsamer Kontext zugewiesen. Hat eine Oberflächenkomponente keinen ihr eigens zugewiesenen Kontext, wird jenes Context-Objekt verwendet, das bei Durchsuchen der Komponentenhierarchie zur Wurzel hin als erstes auftaucht. Für weniger komplexe Benutzeroberflächen genügt es somit, einen einzigen Kontext für die RootPane des beinhaltenden Fensters zu registrieren.

Deaktivieren von Kontexten

In umfangreicheren Benutzeroberflächen ist es häufig der Fall, dass nur Teile der gesamten Applikation gleichzeitig sichtbar sind. Dieser Zustand tritt beispielsweise dann ein, wenn JTabbedPanes zur Präsentation verwendet werden: Sämtliche GUI-Elemente, die sich auf ausgeblendeten Tabs befinden, sind inaktiv. Die Implementierung von Context unterstützt dieses Konzept, indem auch jeder einzelne Kontext aktiv oder inaktiv gesetzt werden kann. Auf einer gegebenen Instanz geschieht dies über die Methode setActive(boolean active). Im Regelfall muss dieser Aufruf manuell erfolgen. Die häufig verwendeten Komponententypen JTabbedPane sowie JDesktopPane bilden jedoch eine Ausnahme, da die Aktivierung ihnen zugeordneter Kontexte durch das Framework automatisch vorgenommen wird.

Notwendig wird die Funktion zur Deaktivierung von Kontexten dort, wo eine Kontexthierarchie existiert, in der die einzelnen Context-Objekte nicht unabhängig voneinander sind.

Aggregationsregeln

Die Abhängigkeit einzelner Context-Objekte voneinander und damit auch die Sichtbarkeit von Kontextinhalten innerhalb der Hierarchie wird über sogenannte Aggregationsregeln gesteuert, die bei der Erstellung des Kontexts angegeben werden. Wird ein Context nach dem in ihm enthaltenen Argumenten befragt, so werden zunächst die Argumente, die dem betreffenden Context selbst hinzugefügt wurden, geliefert. Darüber hinaus können aber auch Argumente von über- oder untergeordneten Kontexten sichtbar werden. Die Sichbarkeitsangaben werden gesteuert über die Werte der Enumerationen Context.Ancestors und Context.Children. Folgende Aggregationsregeln sind möglich:

Children.NONE

Elemente von untergeordneten Kontexten sind niemals sichtbar.

Children.ALL

Die Elemente aller untergeordneter Kontexte sind sichtbar, unabhängig davon, ob diese aktiv oder inaktiv sind.

Children.ACTIVE

Die Elemente der aktiven untergeordneten Kontexte sind sichtbar.

Ancestors.NONE

Elemente von übergeordneten Kontexten sind niemals sichtbar

Ancestors.ALL

Die Elemente aller übergeordneten Kontexte sind sichtbar. Dies bezieht sich jedoch nur auf die Elemente der übergeordneten Kontexte selbst, nicht auf die derer Kindkontexte, unabhängig davon, welche Aggregationsregeln diese benutzen. Elemente von Geschwisterkontexten werden also niemals sichtbar.

Ancestors.PARENT

Die Elemente des direkt übergeordneten Kontexts sind sichtbar, nicht aber die von dessen Kindern.

Ancestors.PARENT_WITH_AGGREGATION

Die Elemente des direkt übergeordneten Kontexts sind sichtbar. Zusätzlich sind die Elemente seiner gesamten Kind-Aggregation sichtbar. Beispiel: Falls der Elternkontext die Aggregationsform Children.ACTIVE verwendet, werden alle Elemente sichtbar, die aktiven Geschwistern des aktuellen Kontexts zugeordnet sind. Auch die weiteren Kind-Hierarchien der Geschwister werden sichtbar.

Verwaltung von Kontexten

Die Verwaltung der Kontexte erfolgt mittels folgender Methoden:

  • static Context.install(JComponent component, Children ca, Ancestors aa) erzeugt einen Context und verbindet ihn mit der gegebenen Komponente. Im Falle des Top-Level-Kontext sollte dieser an der ContentPane des JFrames installiert werden. Verknüpfungen zu bereits vorhandenen Kontexten in der Hierarchie werden automatisch erstellt.

  • static Context.get(JComponent component) liefert den Context, der für die gegebene Komponente zuständig ist. Hierzu wird die Komponentenhierarchie, ausgehend von der gegebenen Komponente nach oben hin durchsucht und der Kontext der ersten Komponente, auf der ein Kontext installiert wurde, zurückgegeben.

  • static Context.getPrivateContext(JComponent component) liefert den Context der Komponente, sofern auf ihr ein Kontext installiert wurde. Andernfalls wird null zurückgegeben. Eine Suche findet also nicht statt.

Hören auf Kontextänderungen

Instanzen der Klasse Context informieren registrierte Interessenten (ContextListener) über Änderungen des Kontexts. Dies betrifft Änderungen an den enthaltenen Objekten, aber auch semantische Änderungen des Kontexts, die provoziert werden können durch einen Aufruf der Methode contextChanged().

Umgang mit Kontextobjekten

In manchen Fällen ist es notwendig, auf die Objekte eines Kontexts gezielt zuzugreifen. Die Klasse ContextUtils bietet zu diesem Zweck eine Sammlung statischer Hilfsmethoden, die folgende Aufgaben erfüllen:

  • Hinzufügen von Objekten

  • Löschen von Objekten

  • Ersetzen von Objekten

  • Suchen von Objekten

  • Ermitteln der Anzahl von Objekten im Kontext.

Die meisten Methoden existieren in zwei Ausprägungen. Die erste akzeptiert ein Context-Objekt auf dem die gewünschte Operation ausgeführt wird. Der zweiten wird eine JComponent übergeben. In diesem Fall wird der gültige Kontext automatisch identifiziert und für die Ausführung der Operation verwendet.

Aktionen (CommandAction)

Aktionen binden ausführbare Kommandos an GUI-Elemente wie Buttons, Toolbar-Buttons oder MenuItems. Sie tragen dabei einerseits zum Erscheinungsbild des GUI-Elements bei, indem sie zum Beispiel Icons oder Labels liefern, steuern andererseits aber auch den Zustand der GUI-Elemente, wie zum Beispiel den enabled/disabled-Zustand. CommandActions können zusätzlich ein oder mehrere Kommandos referenzieren und in ihrer actionPerformed(...) Methode ausführen.

Darüber hinaus ist jede CommandAction an einen Kontext gebunden, dessen Änderung einen Enabled-Check zur Folge hat. Dieser Check schließt einen Check aller enthaltenen Kommandos ein. Nur wenn alle ausführbar sind, setzt sich auch die Aktion als ausführbar.

Die im Kontext enthaltenen Objekte werden den Kommandos zur Ausführung übergeben. Damit bestimmt der Kontext nicht nur über den Enabled-Status einer CommandAction, vielmehr beeinflussen die in ihm enthaltenen Objekte auch die Ausführung der aktivierten Kommandos.

Erweiterungen von CommandActions sind im Allgemeinen nicht notwendig, da die Eigenschaften bestimmt werden durch die Konfigurationsdatei actions.properties.

Kommandos (Command)

Standardaufgaben wie Zoomen, Rotieren oder das Aktivieren von AddOns sind innerhalb der jadice Pakete in jeweils eigenständige Kommandos gekapselt, die von Aktionen aufgerufen werden können und die die eigentliche Ausführung der gewünschten Funktionalität übernehmen. Typische Kommandos benötigen zu ihrer Ausführung eine Anzahl von Objekten, die ihnen über den weiter oben beschriebenen Context zur Verfügung gestellt werden. Damit ein Kommando erfolgreich ausgeführt werden kann, müssen alle von ihm benötigten Objekte im Kontext vorhanden sein. Es ist Aufgabe des Integrators, dies sicherzustellen.

Alle Kommandos sind Instanzen der Klasse InjectedCommand. Zur Ausführung benötigte Objekte werden InjectedCommands via Dependency Injection vollautomatisch und typsicher zur Verfügung gestellt. Es existieren zwei Arten der Injektion. Zum einen können Konfigurationswerte und zum anderen können Kontextobjekte dynamisch bereitgestellt werden. Beide Arten von Dependency Injection werden im Quellcode durch Aufbringen von Java-Annotationen ermöglicht und im Folgenden vorgestellt.

Parameter Injection

Die Parameter-Injection bezieht sich auf Konfigurationswerte, die aus den commands.properties stammen. Um einen Parameterwert zur Verfügung zu stellen, sind folgende Schritte notwendig:

  1. Definition einer Klassenvariablen in der Command-Implementation. Das Framework nimmt selbstständig eine gegebenenfalls notwendige Typ-Konvertierung (beispielsweise für int-Werte) vor.

  2. Erzeugen von public getter/setter Methoden für die Klassenvariable und Aufbringen der Annotation Parameter am Setter.

  3. Eintragen des neuen Parameters in der commands.properties Datei. Die Einträge müssen dem folgenden Schema entsprechen:

    <command-name>.param.<parameter-name>=<parameter-value>

    Der zu verwendende Parametername entspricht dem Methodennamen des setters aber ohne das set-Prefix und mit dem ersten Buchstaben klein geschrieben.

Für Parameter, die auf diese Weise angefordert werden, stellt das Framework sicher, dass diese nach Instanziierung der Klasse bereitstehen.

Für Fälle, in denen der Parametername in commands.properties nicht aus dem Methodennamen generiert werden soll, besteht die Möglichkeit, die Annotation @Parameter mit einem optionalen Argument name zu versehen, das den in der Properties-Datei verwendeten Namen definiert. Des Weiteren existiert ein Annotations-Argument optional, mit dem definiert werden kann, dass das Fehlen der Parameter-Definition in commands.properties nicht zu einem Fehler führen soll. Das Kommando wird dann trotzdem instanziiert und annotierte Felder behalten ihren in der Klasse definierten Default-Wert.

Die Parameter-Injection funktioniert für folgende Datentypen: String, Color, enums und die primitiven Datentypen.

Argument Injection (InjectedCommand)

Die zweite Möglichkeit ist die Argument-Injection. Während Parameter aus den Properties stammen, beziehen sich Argumente auf im Context vorhandene Objekte und werden aus diesem heraus zur Verfügung gestellt.

Das Anfordern von Objekten aus dem Kontext geschieht (analog zur Parameter-Injektion) durch Anbringen der Java-Annotation Argument an einer setter Methode. Das Framework führt einen Aufruf der Methode mit dem ersten im Kontext gefundenen Objekt des entsprechenden Typs durch. Dies geschieht vor einem später eventuell erfolgenden Aufruf der execute()-Methode.

In realen Szenarien kann es leicht vorkommen, dass der Kontext mehrere Objekte desselben Typs enthält. Anstatt einfach das erste Objekt des gewünschten Typs anzufordern, besteht alternativ dazu die Möglichkeit, sämtliche Objekte eines Typs abzufragen. In diesem Fall werden alle gefundenen Objekte in einer Collection des entsprechenden Typs zurückgegeben. So werden beispielsweise mit dem Code-Fragment

@Argument
public void setSomeStrings(Collection<String> allContextStrings){…};

sämtliche String-Instanzen aus dem Kontext angefordert und injiziert.

In Zusammenhang mit Collections existiert eine Einschränkung der InjectedCommands: Es ist nicht möglich, Objekte des Typs Collection im Kontext abzulegen und mittels Argument-Injection auszulesen. Grund hierfür ist die Generic Type Erasure: Zur Laufzeit sind die durch Generics spezifizierten Typinformationen nicht vorhanden, sodass eine korrekte Typkonvertierung nicht zuverlässig möglich ist. Soll eine Ansammlung von Objekten im Kontext abgelegt und injiziert werden, so empfehlen wir dafür ein dediziertes Fachobjekt zu erstellen, das die Objektmenge vorhält.

Die Annotation @Argument bietet verschiedene Optionen an, die den Injektions-Vorgang beeinflussen können. Wie es schon für Parameter der Fall war, können auch Argumente als optional markiert werden. Des Weiteren existiert eine Option boolean allowOtherMatches, die festlegt, ob bei der Abfrage nach einem einzelnen Objekt das Vorhandensein mehrerer gleicher Objekttypen zu einem Fehler führen soll. Die Option boolean matchSubclasses legt fest, ob bei der Suche nach Objekten im Kontext auch Subtypen des angeforderten Typs zurückgeliefert werden sollen. Eine letzte Option, Class<? extends Object> match, definiert nach welchem Objekttyp der Kontext durchsucht werden soll. Wie aus dem Beispiel bereits hervorging, wird der gewünschte Typ im Regelfall automatisch erkannt. Dieses zusätzliche Attribut erlaubt es jedoch, eine nähere Einschränkung – zum Beispiel auf eine bestimmte Subklasse – zu treffen.

Eine letzte Möglichkeit der Argument-Injizierung bietet die Annotation @AllArguments. Diese stellt sämtliche Argumente des Kontextes zur Verfügung. Im Regelfall geschieht dies über einen Rückgabewert des Typs Collection<Object>, wobei zu bedenken ist, dass die Klasse Context das Interface Collection implementiert. Man bekommt daher eigentlich eine Instanz von Context zurück. Um an dieser Stelle keine manuellen Typ-Checks und -Konvertierungen durchführen zu müssen, kann auf der Annotation @AllArguments die Option match = Context.class spezifiziert werden. Ist diese Option vorhanden, so wird direkt eine Instanz von Context zurückgegeben.

[jadice document platform Version 5.5.12.1: Dokumentation für Entwickler. Veröffentlicht: 2021-08-17]