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:
-
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.
-
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.
-
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.
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.
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 JTabbedPane
s 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.
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.
Die Verwaltung der Kontexte erfolgt mittels folgender Methoden:
-
static Context.install(JComponent component, Children ca, Ancestors aa)
erzeugt einenContext
und verbindet ihn mit der gegebenen Komponente. Im Falle des Top-Level-Kontext sollte dieser an der ContentPane desJFrame
s installiert werden. Verknüpfungen zu bereits vorhandenen Kontexten in der Hierarchie werden automatisch erstellt. -
static Context.get(JComponent component)
liefert denContext
, 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 denContext
der Komponente, sofern auf ihr ein Kontext installiert wurde. Andernfalls wirdnull
zurückgegeben. Eine Suche findet also nicht statt.
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()
.
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. CommandAction
s 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 CommandAction
s 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 InjectedCommand
s 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.
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:
-
Definition einer Klassenvariablen in der
Command
-Implementation. Das Framework nimmt selbstständig eine gegebenenfalls notwendige Typ-Konvertierung (beispielsweise fürint
-Werte) vor. -
Erzeugen von public getter/setter Methoden für die Klassenvariable und Aufbringen der Annotation
Parameter
am Setter. -
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 Collection
s existiert eine
Einschränkung der InjectedCommand
s: 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.