Plugin %28Helper%29 erstellen - minova-afis/aero.minova.rcp GitHub Wiki
Hier wird das Erstellen von neuen Plugins für Masken der WFC Anwendung dokumentiert. Alle Funktionen, die nicht zu der Core-Anwendung gehören, werden in Plugins ausgelagert. Mit der Version 12 wird in Masken kein JavaScript mehr ausgewertet. Die entsprechenden Funktionen müssen ebenfalls in einen Helper ausgelagert werden.
Prinzipiell wird für jede Maske ein eigenes Repository und Plugin erstellt.
Grob müssen folgende Schritte befolgt werden:
-
Importieren eines Helpers (wenn der Helper schon im Github liegt und nur weiterentwickelt werden soll)
Diese Schritte sind im Folgenden genauer beschrieben.
In dem Repository für die Erweiterung muss es einen Ordner client
geben. In diesem werden die Resourcen für die WFC-Anwendung abgelegt.
Neben den eigentlichen Plugins müssen noch folgende Dateien erstellt werden:
-
.mvn/extensions.xml
-
minova.target
-
pom.xml
Die .mvn/extensions.xml
Dateil sollte folgenden Inhalt haben:
<extensions>
<extension>
<groupId>org.eclipse.tycho.extras</groupId>
<artifactId>tycho-pomless</artifactId>
<version>2.4.0</version>
</extension>
</extensions>
Damit braucht nicht jeder Unterordner eine eigene pom.xml
um mit Maven gebaut werden zu können.
minova.target
ist die Targetplatform für das Plugin.
Sie kann aus einem bestehenden Projekt kopiert werden, es sind KEINE Anpassungen nötig.
Die Target-Platform des Templates findet sich hier.
Auch die pom kann kopiert werden, es sind allerdings Anpassungen nötig. Komplette Datei für Workingtime hier.
Angepasst werden müssen:
-
artifactId
-
version
-
modules
-
connection und developerConnection
In dem client-Ordner des Repositories wird ein neues Plug-In Project erstellt.
Erste Seite des Dialogs:
-
Name eingetragen, das Namesschema ist
aero.minova.<Projektname>.helper
. -
Location anpassen, es wird der eben erstellte
../client/aero.minova.<Projektname>.helper
Ordner ausgewählt
Zweite Seite des Dialogs:
-
ID eintragen
-
Version (entsprechend der Version des
app
-Teils wenn vorhanden) -
Name ausfüllen/anpassen
-
Java 17 auswählen
Das Manifest des Plugins sollte dann etwa so aussehen. Die Checkbox "Activate this plug-in when one of its classes is loaded" wird gesetzt. Dies ist nötig, damit der Helper zur Laufzeit geladen werden kann.
Ist der Helper schon implementiert und soll nur in Eclipse importiert werden sind diese Schritte zu befolgen. Vorraussetzung: Das Projekt wurde von git geclont.
-
Rechtsklick auf Package Explorer in Eclipse → Import.. → Projects from Git → Existing local repository → Entsprechendes Repository auswählen (muss evtl. erst über Add.. hinzugefügt werden)
-
Import using the New Project wizard
auswählen, in dem Baum zu dem Helper navigieren
-
Plug-in Project
wählen -
Projektnamen eintragen,
../client/aero.minova.<Projektname>.helper
Ordner als Location wählen.
-
Seite 2 des Dialogs bei Default belassen, diese wird eh gleich verworfen (nächster Schritt)
-
MANIFEST.MF Datei mit der HEAD Revision ersetzten (also dem, was schon im git liegt)
Der Helper ist nun als Plugin-Projekt im Eclipse importiert und kann weiterentwickelt werden.
Damit eine Helperklasse geladen werden kann, muss dies in der zugehörigen Maske vermerkt sein, entweder in der Form am Anfang oder in einem Grid. Eine Maske kann auch mehrere Helper haben, z.B. einen vom Anfang der Form, einen pro Grid und einen pro Option Page.
<!-- Vermerk in form-Tag, am Anfang der Maske -->
<form icon="JobDefinition" title="@tJobDefinition.Administration"
helper-class="aero.minova.jobdefinition.helper.JobDefinitionHelper"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../../ch.minova.install/src/ch/minova/install/xsd/XMLForm.xsd">
....
</form>
<!-- Vermerk in einem grid-Tag -->
<grid id="GraduationStep" delete-requires-all-params="true" procedure-suffix="GraduationStep" helper-class="aero.minova.graduation.helper.GraduationStepHelper">
....
</grid>
Die Helperklassen müssen die Klasse IHelper
implementieren. Dafür muss das Package aero.minova.rcp.model
zu den benötigten Plug-Ins hinzugefügt werden.
package aero.minova.jobdefinition.helper;
import aero.minova.rcp.model.form.MDetail;
import aero.minova.rcp.model.helper.ActionCode;
import aero.minova.rcp.model.helper.IHelper;
@Component
public class JobDefinitionHelper implements IHelper {
@Override
public void setControls(MDetail mDetail) {
// TODO Auto-generated method stub
}
@Override
public void handleDetailAction(ActionCode code) {
// TODO Auto-generated method stub
}
}
In der setControls
Methode wird das MDetail übergeben, dass unter anderem alle Felder und Grids enthält. Dieses sollte als lokale Variable gespeichert werden.
Die handleDetailAction
Methode wird aufgerufen, wenn von dem/der Nutzer:in eine Aktion ausgefürt wird. Diese sind im nächsten Abschnitt erklärt.
Außerdem wird an die Klassen @Component
geschrieben.
Dies wird benötigt, damit der Helper zur Laufzeit geladen werden kann.
Sobald mit diesem @Component
gespeichert wird, sollte automatisch ein Ordner OSGI-INF
erstellt werden, der eine XML Datei enthält.
Sollte dies nicht geschehen, kann es über die Einstellungen aktiviert werden:
Das Manifest sollte automatisch um einen Service-Component
-Eintrag erweitert worden sein (zu finden in der Sourcecode-Darstellung, MANIFEST.MF).
Der Ordner OSGI-INF
sollte jetzt noch zu dem Build hinzugefügt werden (Manifest → Tab "Build"
).
Damit der neue Helper entwickelt werden kann, ohne dass dieser erst gebaut und auf dem CAS ausgeliefert werden muss, sind folgende Schritte nötig:
-
In der Debug Configuration → Tracing → aero.minova.rcp.dataservice → debug/uselocalhelper aktivieren
-
Das neue Plugin in die Debug-Configuration von Eclipse aufnehmen, damit die lokale Version genutzt wird (kleiner Pfeil neben Debug-Icon → Plug-ins → entsprechendes Plug-in auswählen)
Alle Helper liegen im Kontext, es ist also möglich Injection zu nutzen.
Über das MDetail kann auf die Felder und Grids zugegriffen werden. Dies funktioniert jeweils über den Feldnamen (Achtung bei OPs, die Felder heißen <opTitel>.<Feldname>
):
MField startDate = mDetail.getField("StartDate");
Beim Betätigen einer Aktion im Detail wird automatisch die Methode handleDetailAction
aufgerufen. Mögliche Aktionen sind:
-
BEFORE-/ AFTERDEL: Vor/Nach dem Löschen eines Eintrages
-
BEFORE-/ AFTERNEW: Vor/Nach Erstellen eines neuen Eintrags, also leeren der Felder. Da hier keine Prozedur aufgerufen wird folgen die Events kurz nacheinander
-
BEFORE-/ AFTERSAVE: Vor/Nach dem Speichern/Updaten eines Eintrages
-
BEFORE-/ AFTERREVERT: Vor/Nach dem Zurücksetzten des Eintrages. Auch hier wird keine Prozedur aufgerufen
-
BEFORE-/ AFTERREAD: Vor/Nach dem Lesen eines Datensatzes, Achtung: AFTERREAD wird verschickt, sobald die HAUPT-Maske komplett geladen ist. Option Pages und Grids sind eventuell noch nicht geladen
ACHTUNG bei den Aktionen VOR Löschen/Speichern/Lesen/…, die Aufrufe sind asynchron, es wird also nicht auf eine Antwort des Helpers gewartet bevor die eigentlichen Prozeduren ausgeführt werden.
Wenn für die Aktion eine Prozedur aufgerufen wird erfolgt das AFTER-Event nur nach erfolgreichem Durchführen dieser.
Für das Logging wird die Klasse org.eclipse.core.runtime.ILog
verwendet.
Der Logger wird über die org.eclipse.core.runtime.Platform
erreicht:
ILog logger = Platform.getLog(this.getClass());
logger.info("Logging Info");
logger.error("Logging Error");
Einzelne Felder können aus dem MDetail
geholt und über die setValue
Methode mit Werten belegt werden. Dabei ist darauf zu achten, dass als Wert ein Value
-Objekt zu verwenden ist, und der Datentyp zu dem Feld passen muss. Das ist besonders wichtig für Lookup
-Values. Als Nutzer wird false
angegeben.
// Feld "startDate" mit aktuellem Datum belegen
MField startDate = mDetail.getField("StartDate");
startDate.setValue(new Value(DateUtil.getDate("0")), false);
// Versuchen, das Feld employee mit dem Lookup-Value für "janiak" vorzubelegen
MLookupField employee = (MLookupField) mDetail.getField("EmployeeKey");
LookupValueAccessor va = (LookupValueAccessor) employee.getValueAccessor();
CompletableFuture<List<LookupValue>> valueFromAsync = va.getValueFromAsync(null, "janiak");
valueFromAsync.thenAccept(l -> Display.getDefault().asyncExec(() -> {
if (!l.isEmpty()) {
LookupValue employeeValue = l.get(0);
employee.setValue(employeeValue, false);
}
}));
Damit das Dirty-Flag richtig funktioniert, müssen die vorbelegten Werte ans WFCDetailCASRequestsUtil geliefert werden.
Das WFCDetailCASRequestsUtil kann injected werden, allerdings ist die Annotation @Optional
zusätzlich nötig.
Die Methode heißt setValueAsCleanForDirtyFlag
und benötigt den neuen Value, den Namen des Feldes (OHNE den Prefix bei OptionPage-Feldern) und den Namen der OptionPage (oder null, wenn das Feld in der Hauptmaske liegt).
@Inject
@Optional
WFCDetailCASRequestsUtil casUtil;
public void preallocateValues() {
employee.setValue(employeeValue, false);
casUtil.setValueAsCleanForDirtyFlag(employeeValue, employee.getName(), null);
bookingDate.setValue(new Value(DateUtil.getDate("0")), false);
casUtil.setValueAsCleanForDirtyFlag(bookingDate.getValue(), bookingDate.getName(), null);
}
Einzelne Werte in Grids können direkt in die zugrundeliegende Tabelle gesetzt werden, das Grid wird automatisch aktualisiert.
Die Klasse aero.minova.rcp.model.Table
enthält einige Methoden die das Setzen von Werten weiter vereinfachen, etwa kann statt dem Spaltenindex auch der Spaltenname angegeben werden.
//setValue(int columnIndex, int rowIndex, Value newValue)
mDetail.getGrid("InvoicePosition").setValue(4, 5, new Value("neuer Wert"))
Um ganze Zeilen hinzuzufügen wird eine Tabelle verwendet. Die Spaltennamen in der übergebenen Tabelle müssen mit denen des Grids übereinstimmen, da diese genutzt werden um die Werte an die richtige Stelle zu schreiben. Die Reihenfolge der Spalten muss also NICHT übereinstimmen und es muss auch nicht die gleiche Anzahl sein. Wenn eine Spalte des Grids in der übergebenen Tabelle nicht gefunden wurde bleibt die entsprechende Zelle im Grid leer.
mDetail.getGrid("InvoicePosition").addRows(tableWithNewRows);
Wenn die Daten eines Grids komplett ersetzt werden sollen und auch das Dirtyflag nicht anspringen soll muss das Setzen über die Klasse WFCDetailCASRequestsUtil
geschehen.
Wie für die Zeilen müssen die Spaltennamen übereinstimmen, Reihenfolge ist aber egal.
@Inject
@Optional
WFCDetailCASRequestsUtil casUtil;
public void setContent() {
casUtil.setGridContent(mDetail.getGrid("InvoicePosition"), newDataTable);
}
Es ist möglich, Eingaben in Grids direkt zu validieren.
Dafür muss eine Klasse das Interface IGridValidator
implementieren.
Über die Methode MGrid#addValidation(IGridValidator validator, List<Integer> columnsToValidate)
wird die Validierung hinzugefügt.
Die Methode checkValid()
wird genutzt, um ungültige Zellen rot zu färben und das Speichern zu verhindern.
Mit validateThrowingException()
wird die Eingabe überprüft, bevor sie ins Grid eingetragen wird.
Bei dem Aufruf steht der neue Wert also noch nicht in der Tabelle!
Wenn der neue Wert nicht gültig ist, muss eine ValidationFailedException
geworfen werden.
Deren Text wird übersetzt und als Notification-Popup unten links angezeigt.
Die Spalten- und Zeilenindices entsprechen den Positionen in der Tabelle, die dem Grid zugrunde liegt. Auf die Werte kann also wie gewohnt über das MGrid zugegriffen werden.
Die beiden Methoden werden nur für Werte aufgerufen, die in einer Spalte stehen deren Index in den columnsToValidate
steht.
Hier sollte darauf geachtet werden, dass die meisten Grids unsichtbare Spalten haben, in denen die Keys stehen, die nicht validiert werden sollten.
In dem folgenden Beispiel werden die Werte der Spalte 2 (eine Integer Spalte) darauf überprüft, ob sie kleiner als 10 sind:
@Component
public class GraduationStepHelper implements IHelper, IGridValidator {
private MGrid graduationSteps;
@Override
public void setControls(MDetail mDetail) {
graduationSteps = mDetail.getGrid("GraduationStep");
List<Integer> columnsToValidate = List.of(2); // Es soll nur Spalte 2 überprüft werden
graduationSteps.addValidation(this, columnsToValidate);
}
@Override
public void handleDetailAction(ActionCode code) {
// Es werden nur Einträge ins Grid überprüft, nicht die Aktionen
}
@Override
public boolean checkValid(int columnIndex, int rowIndex) {
return graduationSteps.getDataTable().getValue(columnIndex, rowIndex).getIntegerValue() <= 10;
}
@Override
public void validateThrowingException(int columnIndex, int rowIndex, Object newValue) throws ValidationFailedException {
if ((Integer) newValue > 10) {
throw new ValidationFailedException("Wert darf nicht größer als 10 sein!");
}
}
}
Um auf Wertänderungen reagieren zu können, muss eine Klasse den ValueChangeListener
bzw. den GridChangeListener
implementieren.
Diese Klasse kann dann als Listener zu einem Feld oder Grid hinzugefügt werden um auf Wertänderungen zu reagieren.
TicketHelper ticketHelper = new TicketHelper(this);
mDetail.getField("OrderReceiverKey").addValueChangeListener(ticketHelper);
public class TicketHelper implements ValueChangeListener {
@Override
public void valueChange(ValueChangeEvent evt) {
MLookupField lookupField = (MLookupField) evt.getField();
String writtenText = lookupField.getWrittenText();
if (writtenText != null && writtenText.startsWith("#")) {
System.out.println("Eingegbenes Ticket: " + writtenText);
}
}
}
Mit der Methode ValueChangeEvent#isUser
kann herausgefunden werden, ob die Änderung durch die Nutzer:in oder das System selbst ausgelöst wurde.
Außerdem sind der alte sowie der neue Wert im Event gespeichert.
Es gibt vier verschiedene Typen von GridChangeEvents. Jedes GridChangeEvent hat einen GridChangeType und entsprechend sind die Attribute (nicht) gesetzt.
-
RESET: Die Tabelle wurde komplett geändert (z.B. nach dem Laden eines neuen Datensatzes)
-
INSERT: Eine neue Zeile wurde ins Grid eingefügt. Der Index der neuen Zeile sowie die Zeile selbst wird mitgegeben
-
DELETE: Eine Zeile des Grids wurde gelöscht. Der Index, den die Zeile hatte, sowie die Zeile selbst wird mitgegeben
-
UPDATE: Ein einzelner Wert im Grid wurde geändert. Zeilen- und Spaltenindex sowie der alte und neue Wert und die geänderte Zeile werden mitgegeben
Um herauszufinden, ob das Detail "Dirty" ist (also es Änderungen seit dem letzten Speichern/Lesen/Leeren des Details gab) wird das DirtyFlag ausgelesen.
Dazu wird die DirtyFlagUtil
injected (die @Optional
Annotation muss hinzugefügt werden, um eine Exception beim Starten zu vermeiden).
@Inject
@Optional
DirtyFlagUtil dirtyFlagUtil;
public boolean checkDirty() {
return dirtyFlagUtil.checkDirty(); // True wenn es eine Änderung gab, false ansonsten
}
Alle Knöpfe funktionieren aus Sicht der Helper gleich, egal ob sie in der Maske in der Kopf-Section, einer nicht-Kopf-Section oder in Grids definiert wurden (Einziger kleiner Unterschied bei gruppierten Knöpfen, siehe weiter unten).
In Version 11 konnten Buttons/Knöpfe über die Maske de-/aktiviert werden, je nachdem ob ein gewisses Feld einen Wert hat:
<!-- Alte Maske -->
<button icon="Print.Command" text="@Action.PrintInvoice" id="PrintInvoice">
<dynamic property="enabled">KeyLong != null</dynamic>
</button>
Da wir kein JavaScript mehr in der Maske wollen, kann der entsprechende Code etwa wie folgt aussehen. Damit der Button immer aktuell bleibt, sollte das De-/Aktivieren mit einem ValueChangeListener auf das entsprechende Feld verbunden werden.
MField keyLong = mDetail.getField("KeyLong");
MButton printInvoice = mDetail.getButton("PrintInvoice");
printInvoice.setEnabled(keyLong.getValue() != null); // Button initial richtig de/aktivieren
keyLong.addValueChangeListener(evt -> { // Auf Änderung des KeyLongs reagieren
printInvoice.setEnabled(keyLong.getValue() != null);
});
Manchmal ist es nötig, dass ein Button nur aktiv ist, wenn mindestens eine Zeile in einem Grid ausgewählt ist. Das lässt sich wie folgt umsetzten:
MButton button = mDetail.getButton("ButtonName");
MGrid grid = mDetail.getGrid("GridName");
grid.addSelectionListener(event -> {
if (event instanceof ISelectionEvent) {
button.setEnabled(!grid.getSelectedRows().getRows().isEmpty());
}
});
Außerdem muss ein Helper auf das Drücken eines Buttons reagieren können.
Dafür kann ein SimpleSelectionAdapter
auf den Button registriert werden:
MButton printInvoice = mDetail.getButton("PrintInvoice");
printInvoice.addSelectionListener(new SimpleSelectionAdapter() {
@Override
public void handle(SelectionEvent e) {
// Entsprechende Methode(n) ausführen
}
});
ACHTUNG bei Buttons mit Dropdown in Abschnitten
In Version 12 können mehrere Knöpfe zu einer Gruppe zusammengefasst werden (group
-Tag in der xml).
Diese werden dann als Dropdown dargestellt.
Befinden sich diese Knöpfe NICHT in den Kopfdaten muss im Helper eine if-Bedingung eingefügt werden, um zu verhindern, dass die Methoden ausgeführt werden, wenn auf den Pfeil geklickt wird oder der Button disabled ist.
Sind die Knöpfe in den Kopfdaten muss nichts weiter beachtet werden.
<!-- Beispiel für gruppierte Knöpfe -->
<page icon="Calendar.ico" id="Administration" text="@tJournaltemplate.Group.Administration">
<button id="insertTemplates" group="insert" icon="CompleteRecords.Command" text="@tJournaltemplate.action.insertTemplates"/>
<button id="insertSingleTemplate" group="insert" icon="CompleteRecord.Command" text="@tJournaltemplate.action.insertSingleTemplate"/>
...
</page>
MButton insertTemplates = mDetail.getButton("insertTemplates");
insertTemplates.addSelectionListener(new SimpleSelectionAdapter() {
@Override
public void handle(SelectionEvent e) {
// Die if-Bedingung verhindert, dass die Methoden ausgeführt werden, wenn auf den Pfeil geklickt wird oder der Button disabled ist
if (e.detail != SWT.ARROW && insertTemplates.isEnabled()) {
// Entsprechende Methode(n) ausführen
}
}
});
Es gibt zwei Möglichkeiten, die Einstellungen der Anwendung auszulesen. Mit Möglichkeit eins wird der Wert einer bestimmten Einstellung direkt injected. Dieser Wert wird auch automatisch aktualisiert, wenn sich die Einstellung ändert.
@Inject
@Preference(nodePath = ApplicationPreferences.PREFERENCES_NODE, value = ApplicationPreferences.AUTO_RELOAD_INDEX)
boolean autoReloadIndex;
Mit Möglichkeit zwei werden die gesamten Einstellungen ausgelesen. Das bietet die Möglichkeit, bei Änderungen einer Einstellung direkt zu reagieren.
// Preferences laden (Kann auch am Anfang der Klasse geschehen)
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(ApplicationPreferences.PREFERENCES_NODE);
// Wert auslesen
boolean autoReloadIndex = preferences.getBoolean(ApplicationPreferences.AUTO_RELOAD_INDEX, false);
// Bei einer Änderung der Einstellung reagieren
preferences.addPreferenceChangeListener(event -> {
if (event.getKey().equals(ApplicationPreferences.AUTO_RELOAD_INDEX)) {
// TODO
}
});
In Version 11 konnten einzelne Sections über die Maske ein- oder ausgeblendet werden.
<!-- Alte Maske -->
<page id="Debug" text="@Administration" icon="Administration" visible="false">
<dynamic property="visible">app.isSUMode()</dynamic>
...
</page>
Auch dies wird in Version 12 über einen Helper umgesetzt.
Vorerst gibt es keinen Super User Modus mehr, stattdessen können versteckte Abschnitte über die Einstellungen eingeblendet werden ("Darstellung" Tab, die Einstellung hat ID ApplicationPreferences.SHOW_HIDDEN_SECTIONS
).
Die Umsetzung im Helper kann dann wie folgt aussehen:
//Preferences laden
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(ApplicationPreferences.PREFERENCES_NODE);
//MSection über ihre ID aus dem MDetail holen
MSection debugSection = mDetail.getPage("Debug");
// Initial Sichtbarkeit entsprechend der Einstellung setzten
debugSection.setVisible(preferences.getBoolean(ApplicationPreferences.SHOW_HIDDEN_SECTIONS, false));
// Bei einer Änderung der Einstellung Sichtbarkeit anpassen
preferences.addPreferenceChangeListener(event -> {
if (event.getKey().equals(ApplicationPreferences.SHOW_HIDDEN_SECTIONS)) {
debugSection.setVisible(event.getNewValue().equals("true"));
}
});
Felder und Grids können über die Helper auf read-only oder required gesetzt werden. Das Verhalten (kein Speichern ohne Value bei required und kein Bearbeiten von read-only Feldern) sowie die Darstellung wird automatisch angepasst. In Grids können aktuell nur ganze Spalten geändert werden. Für Grids gibt es die Möglichkeit, das ganze Grid auf einmal zu ändern. Zudem bietet das MDetail Methoden, mit dem alle Felder oder alle Felder und Grids auf einmal angepasst werden können. Auch das Zurücksetzen auf den Originalzustand (wie in der Maske definiert) ist möglich.
// Felder
mDetail.getField("Description").setReadOnly(true);
mDetail.getField("Description").setRequired(true);
mDetail.getField("Description").resetReadOnlyAndRequired();
// Grids, äquivalent für required möglich
mDetail.getGrid("InvoicePosition").setColumnReadOnly(4, true);
mDetail.getGrid("InvoicePosition").setGridReadOnly(true);
mDetail.getGrid("InvoicePosition").resetReadOnlyAndRequiredColumns();
// Über mDetail alle Felder / Grids ändern
mDetail.setAllFieldsReadOnly(true);
mDetail.setAllGridsAndFieldsReadOnly(true);
mDetail.resetAllGridsAndFieldsReadOnlyAndRequired();
Die Sichtbarkeit von Feldern kann über einen Helper geändert werden. Achtung beim unsichtbar-setzen von required Feldern! Das Feld muss trotzdem gefüllt sein, damit gespeichert werden kann, über die UI ist das aber nicht mehr möglich. Es gibt auch die Möglichkeit, die Sichtbarkeit auf den Originalzustand (wie in der Maske definiert) zurückzusetzen. Da bei jeder Änderung der Sichtbarkeit eines Feldes der gesamte Abschnitt neu gezeichnet werden muss, sollten diese Methoden sparsam eingesetzt werden.
mDetail.getField("Description").setVisible(false); // Feld unsichtbar setzten
mDetail.getField("Description").resetVisibility(); // Sichtbarkeit zurücksetzten
mDetail.resetAllFieldsVisibility(); // Sichtbarkeit aller Felder zurücksetzten
In der alten Version wurden die Felder, die in Param-String Feldern dargestellt werden über JavaScript in der Maske gesetzt:
<!-- Alte Maske -->
<jscript>
tools.setProp('ExecutionParameter', 'xml-file', 'SQL2FileParameter.op.xml');
</jscript>
In Version 12 muss dies über einen Helper geschehen, indem die entsprechende Methode der Klasse WFCDetailCASRequestsUtil
aufgerufen wird.
Wenn noch nicht geschehen, wird die Maske automatisch runtergeladen und ausgelesen, die entsprechenden Felder werden im Detail erstellt.
Der äquivalente Code kann so aussehen:
@Inject
@Optional // Ohne @Optional tritt ein Fehler auf
WFCDetailCASRequestsUtil casUtil;
public void updateParamString() {
casUtil.updateParamStringField((MParamStringField) mDetail.getField("ExecutionParameter"), "SQL2FileParameter.op.xml");
}
In der alten Version wurden über JavaScript Werte aus der Datenbank geholt:
<!-- Alte Maske -->
<jscript>
tools.getSQLValue('vJobDefinitionParameter', 'JobExecutorKey', JobExecutorKey, 'ClassName');
</jscript>
Dies kann in der neuen Version wie folgt abgebildet werden (Der IDataService
kann injected werden):
Value jobExecuterKey = mDetail.getField("JobExecutorKey").getValue();
dataService.getSQLValue("vJobDefinitionParameter", "JobExecutorKey", jobExecuterKey, "ClassName", mDetail.getField("ClassName").getDataType());
Die Methode getSQLValue() nimmt folgende Parameter:
-
String tablename: der Name der angefragten Tabelle/ View
-
String requestColumn: der Name der Spalte für die der Wert gegeben ist
-
Value requestValue: der Wert nach dem gesucht werden soll (in der gegebenen Spalte)
-
String resultColumn: der Name der Spalte, für die der Wert zurückgegeben werden soll
-
DataType resultType: der Typ des angefragten Werts
Manchmal kann es gewünscht sein, dass die Lookup-Werte nicht alphabetisch sortiert werden.
Dafür kann Lookup Feldern ein Comparator<LookupValue>
gegeben werden.
In dem Beispiel werden die Optionen numerisch aufsteigend sortiert (es wird davon ausgegangen, dass alle KeyText zahlenwerte sind).
MLookupField issue = (MLookupField) mDetail.getField(WORKINGTIME_GITHUB_ISSUE);
issue.setComparatorForLookupContentProvider(new SortByNumber());
private class SortByNumber implements Comparator<LookupValue> {
@Override
public int compare(LookupValue o1, LookupValue o2) {
return Integer.parseInt(o1.keyText) - Integer.parseInt(o2.keyText);
}
}
Manchmal kann es gewünscht sein, dass für Lookup-Felder nicht alle Optionen, die von der Datenbank geliefert werden, auch angezeigt werden.
Entsprechend kann Lookup Feldern ein Filter gegeben werden.
Dieser muss von die Klasse Predicate<LookupValue>
haben.
In dem Beispiel wird das LookupValue mit KeyText "AUTOCREDIT" aus den angezeigten Optionen entfernt.
MLookupField invoiceTypeKey = (MLookupField) mDetail.getField("InvoiceTypeKey");
invoiceTypeKey.setFilterForContentProvider(value -> !value.getKeyText().equals("AUTOCREDIT"));
Auch in Version 12 gibt es eine Tabelle tSiteParameter
.
Deren Werte können über den IDataService
(der injected werden kann) angefragt werden.
Als Parameter wird der Key sowie der Defaultwert (falls der Key nicht existiert) benötigt.
Es wird immer ein String zurückgegeben.
Siehe auch Trac Doku für alle möglichen SiteParameter und ihre Defaults.
String defaultCurrency = dataService.getSiteParameter("InvoiceDefaultCurrency", "2");
Alle Handler können auch aus dem Code aufgerufen werden.
Dafür müssen der ECommandService
und der EHandlerService
injected werden.
Wenn die canExecute()
Methode des jeweiligen Handlers false zurückgibt wir der Handler auch nicht ausgeführt.
//Beispielhafes Ausführen des Handlers zum Speichern des Details
String commandID = Constants.AERO_MINOVA_RCP_RCP_COMMAND_SAVEDETAIL
ParameterizedCommand cmd = commandService.createCommand(commandID, null);
handlerService.executeHandler(cmd);
Es gibt prinzipiell zwei Möglichkeiten, Nachrichten anzeigen zu lassen. Zum einen kann ein kleines Popup erstellt werden, das auch ohne Interaktion wieder verschwindet. Die andere Möglichkeit ist als Fehlermeldung, die aktiv weggeklickt werden muss.
Beide Methoden werden über den IEventBroker
aufgerufen.
Der Nachrichtstring wird automatisch übersetzt, ob mit oder ohne "@" am Anfang.
Außerdem wird der übersetzte String formattiert, wobei die einzusetzenden Parameter mit %
getrennt werden.
Beispiel:
Aus dem String @msg.TextTooLong %110>100
und der zugehörigen Übersetzung in den .messagesProperties: Maximale L\u00E4nge \u00FCberschritten ({0})!
ergibt sich die Übersetzung Maximale Länge überschritten (110>100)!
.
broker.send(Constants.BROKER_SHOWNOTIFICATION, "msg.notification"); // Popup
broker.send(Constants.BROKER_SHOWERRORMESSAGE, "msg.errormessage"); // Fehlermeldung
broker.send(Constants.BROKER_SHOWERRORMESSAGE, "msg.TextTooLong %110>100"); // Fehlermeldung mit Parametern
Es können Dialoge geöffnet werden, die Hinweise anzeigen oder Auswahlmöglichkeiten zwischen mehreren vorgefertigten Optionen geben.
MessageDialog dialog = new MessageDialog(
Display.getDefault().getActiveShell(), // Aktive Shell auslesen
translationService.translate("@msg.QuestionTitle", null), // Titel (übersetzt)
null, // Titelbild (meistens leer)
translationService.translate("@msg.QuestionMessage", null), // Nachricht (übersetzt)
MessageDialog.QUESTION, // Art des Icons im Dialog, andere Möglichkeiten siehe MessageDialog
new String[] { translationService.translate("@Yes", null), // Array mit den Knöpfen, kann auch nur einer sein
translationService.translate("@No", null) },
0); // Index der vorausgewählten Option (wird bei Enter automatisch ausgewählt)
int result = dialog.open(); // Dialog öffnen und Ergebnis auslesen (Index der gewählten Option)
if (result == 0) { // Überprüfen, was geklickt wurde
// TODO: Entsprechende "Yes" Methode ausführen
} else {
// TODO: Entsprechende "No"/ Abbrechen Methode ausführen
}
Um Werte aus der .xbs auszulesen bietet die Klasse XBSUtil
einige Methoden an.
Die Preferences
(also die gesamte .xbs) sind über die MApplication
(kann injected werden) erreichbar.
Preferences preferences = (Preferences) mApplication.getTransientData().get(Constants.XBS_FILE_NAME); // Gesamte .xbs
Node node = XBSUtil.getNodeWithName(preferences, "NodeName"); // ERSTE Node mit gegebenem Namen
Map<String, String> mapOfNode = XBSUtil.getMapOfNode(node); // Map der Node
Map<String, String> mapOfNode = XBSUtil.getMapOfNode(preferences, "NodeName"); // Äquivalent zu oberem Aufruf
Es ist möglich, für eine Masken ein zusätzliches Tab in den Einstellungen hinzuzufügen (z.B. um den/die Mitarbeiter:in in der Stundenerfassung vorzubelegen).
Dafür muss eine neue Klasse definiert werden, die die Klasse PreferenceTabDescriptor
erweitert.
Das verwendete Icon muss (wie die andern Bilder der Maske) im src/main/app/images/
Ordner abgelegt werden.
Die Einstellungen werden dann wie auch die Haupteinstellungen aufgebaut, siehe auch Klasse PreferenceWindowModel
.
Auf die eingestellten Werte kann dann wie für alle anderen Einstellungen auch zugegriffen werden.
public class WorkingTimePreferencePage extends PreferenceTabDescriptor {
public WorkingTimePreferencePage() {
super("aero.minova.rcp.preferencewindow", "icons/SIS.png", "sisTab", getTranslationService().translate("@Preferences.WorkingTime", null), 0.6);
TranslationService translationService = getTranslationService();
PreferenceSectionDescriptor psd;
psd = new PreferenceSectionDescriptor("user", translationService.translate("@Preferences.WorkingTime.UserPreselect", null), 0.1);
this.add(psd);
psd.add(new PreferenceDescriptor(Constants.USER_PRESELECT_DESCRIPTOR,
translationService.translate("@Preferences.WorkingTime.UserPreselectDescription", null), 0.1, DisplayType.STRING,
System.getProperty("user.name")));
}
/**
* Translation Service aus dem Kontext holen
*/
private static TranslationService getTranslationService() {
IEclipseContext serviceContext = EclipseContextFactory.getServiceContext(FrameworkUtil.getBundle(WorkingTimePreferencePage.class).getBundleContext());
return serviceContext.get(IWorkbench.class).getApplication().getContext().get(TranslationService.class);
}
}
Wenn die Klasse erstellt ist muss sie noch in der Manifest.MF
Datei eingetragen werden.
Unter Extensions
wird minova.preferencepage
ausgewählt und die eben erstellte Klasse wird als page eingetragen.
Atomatisch wird ein plugin.xml
file erstellt, das folgenden Inhalt haben sollte (als Klasse natürlich die eben erstellte):
<plugin>
<extension
point="minova.preferencepage">
<page
class="aero.minova.workingtime.preferencepage.WorkingTimePreferencePage">
</page>
</extension>
</plugin>
Masken können um einen Knopf erweitert werden, der einen Wizard öffnet. Dafür muss prinzipiell kein Helper erstellt werden. Das Erstellen des Plugins funktioniert äquivalent.
Die Wizards sind dafür gedacht, die Möglichkeit für komplexe Eingaben zu bieten. Wenn nur eine Frage mit mehreren vorgefertigten Antwortmöglichkeiten (z.B. "Soll gespeichert werden? Ja/Nein") gestellt wird, sollte besser ein Dialog genutzt werden.
Um den Wizard zu öffnen muss er in der Maske vermerkt sein. Zudem sit wichtig, dass ein Helper angegeben ist, damit das Plugin geladen wird.
<detail ...>
<head>
<!-------Button der den Wizard öffnen soll----->
<button icon="AutoInvoice.Command" text="@AutoInvoiceWizard.Button" id="AutoInvoice"/>
....
</head>
....
</detail>
<events>
<onclick refid="AutoInvoice">
<!-------Name des Wizards----->
<wizard wizardname="aero.minova.invoice.wizard.AutoInvoiceWizard"/>
</onclick>
</events>
Jeder Wizard besteht aus mindestens drei Klassen.
-
Der Wizard selbst
-
Mindestens einer Page
-
Der FinishAction
@Component
public class AutoInvoiceWizard extends MinovaWizard implements IMinovaWizard {
private AutoInvoicePage1 page1;
private AutoInvoicePage2 page2;
public AutoInvoiceWizard() {
super("@AutoInvoice.WizardName");
this.setFinishAction(new AutoInvoiceFinishAction());
}
@Override
public void addPages() {
// Die Pages sollen nur hinzugefügt werden, wenn noch keine Pages erstellt wurden
if (this.getPages().length != 0) {
return;
}
//Titel übersetzen
this.setWindowTitle(translationService.translate("@AutoInvoice.WizardName", null));
// Pages erstellen
page1 = new AutoInvoicePage1(translationService.translate("@AutoInvoice.Page1Name", null),
translationService.translate("@AutoInvoice.Page1Description", null));
addPage(page1);
page2 = new AutoInvoicePage2(translationService.translate("@AutoInvoice.Page2Name", null),
translationService.translate("@AutoInvoice.Page2Description", null));
addPage(page2);
super.addPages();
}
}
Wie auch bei Helpern muss @Component
an die Klasse geschrieben werden.
Außerdem wird implements IMinovaWizard
benötigt, damit der Wizard zur Laufzeit geladen werden kann.
Wie bei dem Helper sollte automatisch im Ordner OSGI-INF
eine neue .xml Datei für den Wizard angelegt worden sein.
Der MinovaWizard
wird extended, um die Konfiguration zu erleichtern.
Etwa wird die FinishAction
automatisch aufgerufen und die Pages liegen im Kontext.
Außerdem sind hier schon einige oft benutzte Klassen wie der translationService
oder der dataService
injected, auf diese kann direkt zugegriffen werden.
Im Konstruktor wird der Name gesetzt (dieser wird allerdings noch nicht übersetzt).
Außerdem wird die passende FinishAction
hinzugefügt.
addPages
Methode:
-
Die if-Abfrage verhindert, dass die Pages mehrmals hinzugefügt werden
-
Der Titel wird übersetzt (inzwischen ist der TranslationService verfügbar)
-
Die Pages werden erstellt
-
super.addPages()
ist wichtig, damit die Pages automatisch im Kontext liegen (und Injection verfügbar ist). Außerdem werden einige Listener für bessere Bedienbarkeit registriert
Jeder Wizard benötigt mindestens eine Page.
Es gibt prinzipiell zwei verschiedene Arten von Pages, mit Feldern oder mit Tabellen.
Beide Arten von Pages extenden die MinovaWizardPage
!
In dieser wird dafür gesorgt, dass die Knöpfe entsprechend de-/aktiviert werden, wenn alle Pflichtfelder gefüllt sind.
Außerdem steuert sie das Enterverhalten und bietet Methoden für das Erstellen von Feldern.
public class AutoInvoicePage1 extends MinovaWizardPage {
protected AutoInvoicePage1(String pageName, String pageDescription) {
super(pageName, pageName, pageDescription);
}
@Override
public void createControl(Composite parent) {
// Composite mit Layout erstellen
Composite composite = getComposite(parent);
// "Normales" Feld
MField bookingDate = createMField(DataType.INSTANT, DateTimeType.DATE, "@AutoInvoice.InvoiceDate", true);
createUIField(bookingDate, DateTimeType.DATE, composite, 0, 0);
// Feld mit weiteren Konfiguration
MField descriptionField = createMField(DataType.STRING, null, "@WorkingTime.Description", true);
descriptionField.setNumberColumnsSpanned(4);
descriptionField.setNumberRowsSpanned(3);
descriptionField.setFillHorizontal(true);
createUIField(descriptionField, null, composite, 4, 0);
// Lookup Feld
Lookup employeeLookup = new Lookup();
employeeLookup.setTable("tEmployee");
MField employeeField = createMLookupField(employeeLookup, "@WorkingTime.EmployeeText", true);
createUILookupField(employeeField, composite, 0, 0);
employeeField.setValue(originalMDetail.getField("EmployeeKey").getValue(), false);
// Konfiguration abschließen
TranslateUtil.translate(composite, translationService, locale);
valueChange(null); // Update der Knöpfe
init();
}
@Override
public void valueChange(ValueChangeEvent evt) {
// TODO Weitere Validierung der Felder
super.valueChange(evt); // Update der Knöpfe!
}
}
createControl()
Methode:
-
Erstellen eines Composites. Hierfür wird die Methode aus der
MinovaWizardPage
verwendet, die sich um das Layout kümmert. -
Erstellen der Felder (auf dem Composite). Dies geschieht in zwei Schritten, zuerst das MField, dann das UI. Damit kann etwa die Breite der Felder noch angepasst werden. Lookup-Felder benötigen zudem ein
aero.minova.rcp.form.model.xsd.Lookup
, in dem die gewünschte Tabelle/Prozedur angegeben ist -
Übersetzen der Felder mit
TranslateUtil.translate(composite, translationService, locale)
-
Aktualisieren der Knöpfe mit
valueChange(null)
-
Zuletzt
init()
aufrufen, damit dieonSelect()
Methode zur verfügung steht
Weitere Validierungen können (wie bei Helpern) über die valueChange()
Methode vorgenommen werden.
Da schon die MinovaWizardPage
den ValueChangeListener
implementiert ist dies nicht nochmal nötig.
Wichtig ist, dass am Ende der Methode super.valueChange(evt)
aufgerufen wird, damit die Knöpfe richtig aktualisiert werden!
public class AutoInvoicePage2 extends MinovaWizardPage {
protected AutoInvoicePage2(String pageName, String pageDescription) {
super(pageName, pageName, pageDescription);
}
@Override
public void createControl(Composite parent) {
Composite composite = getCompositeNattable(parent);
MinovaWizardIndexUtil indexUtil = new MinovaWizardIndexUtil("AutoInvoiceContracts.xml");
ContextInjectionFactory.inject(indexUtil, context);
NatTable natTable = indexUtil.createNattable(composite);
setPageComplete(false);
init();
}
@Override
protected void onSelect() {
// TODO: Prozedur aufrufen um Tabelle zu füllen
setPageComplete(true);
}
}
createControl()
Methode:
-
Erstellen eines Composites für die NatTable
-
Erstellen der NatTable über Klasse
MinovaWizardIndexUtil
. Diese lädt automatisch (wenn nötig) das angegebene Formular vom CAS und baut die NatTable auf. Dafür muss die Klasse im Kontext liegen. Die Tabelle wird automatisch übersetzt -
setPageComplete(false)
aufrufen, damit der Wizard nicht von vornherein abschließbar ist. -
Zuletzt
init()
aufrufen, damit dieonSelect()
Methode zur verfügung steht
Die onSelect()
Methode wird aufgerufen, sobald die Nutzer:in diese Page öffnet.
Hier kann dann z.B. die Tabelle über eine Prozedur gefüllt werden.
Entsprechend sollte auch setPageComplete(true)
aufgerufen werden, damit der Wizard dann abgeschlossen werden kann.
public class AutoInvoiceFinishAction implements IMinovaWizardFinishAction {
private AutoInvoiceWizard minovaWizard;
@Override
public void setWizard(MinovaWizard minovaWizard) {
this.minovaWizard = (AutoInvoiceWizard) minovaWizard;
}
@Override
public boolean execute() {
//TODO: Wizard abschließen
return true;
}
}
Jede FinishAction implementiert die IMinovaFinishAction
.
In der Methode setWizard()
wird der Wizard übergeben.
Die Methode execute()
wird aufgerufen, sobald die Nutzer:in auf "Abschließen" klickt.
Hier sollte dann also entsprechend eine Prozedur aufgerufen oder der Wizard anderweitig abgeschlossen werden.
Wichtig ist die Rückgabe.
-
Bei
true
wird der Wizard automatisch geschlossen -
Bei
false
bleibt der Wizard geöffnet
Hintergrund: In Helpern können erstmal nur Libraries genutzt werden, die auch in der WFC-Anwendung eingebunden sind. Für einige Funktionalitäten werden allerdings weitere Abhängigkeiten benötigt (z.B. für API-Anbindungen). Ist dies der Fall müssen diese Abhängigkeiten (als jars) mit dem Helper ausgeliefert werden, damit der Helper von der Anwendung genutzt werden kann. Da wir in der Anwendung und in den Helpern pomless arbeiten, reicht es leider nicht, nur eine Dependency hinzuzufügen.
Zuerst muss die pom (sollte im client
-Ordner liegen) um die entsprechenden Abhängigkeiten erweitert werden.
Ziel ist, dass die jars in einen Ordner kopiert werden, von dem aus sie im Helper genutzt werden können.
Dafür ist folgendes Plugin nötig:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy-libraries</id>
<phase>validate</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems> <!-- Hier Abhängigkeiten eintragen (können auch mehrere items sein) -->
<item> <!-- Beispiel -->
<groupId>aero.minova</groupId>
<artifactId>cas.openapi.api.client</artifactId>
<version>1.0.4</version>
</item>
</artifactItems>
<outputDirectory>lib</outputDirectory> <!-- Name des Output-Ordners -->
<stripVersion>true</stripVersion>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</execution>
</executions>
</plugin>
Wenn die pom angepasst ist muss der Helper einmal gebaut werden, damit der Ordner mit den jars erstellt wird.
Wird eine neue Version benötigt, kann diese einfach in der pom eingetragen werden, der Helper muss erneut gebaut werden.
Damit die Abhängigkeiten im Code verwendet werden können muss die MANIFEST.MF Datei wie folgt angepasst werden:
Runtime → Classpath → Add… → Entsprechende jars auswählen
Außerdem müssen die jars mit dem Helper ausgeliefert werden:
Build → Entsprechende jars auswählen (es kann auch gleich der gesamte Ordner gewählt werden)
Nun kann ganz normal mit der Abhängigkeit gearbeitet werden, eventuell ist noch ein Eintrag in Dependencies → Required Plug-ins
nötig.
Das Plugin kann gebaut werden, indem der mvn clean verify
Befehl in dem client
Ordner des entsprechenden Repositories ausgeführt wird. Das gebaute .jar File liegt dann im target
-Ordner des jeweiligen Plugins.
Alternativ kann auch ein Github-Workflow angelegt werden, der zusätzlich zum App Teil auch den Helper baut. Beispiel für Workingtime hier.
Dieser kann über Actions → Release App and Helper
genutzt werden. Der Helper wird dann mit gebaut und in den src/main/app/plugins
Ordner abgelegt, von wo er automatisch vom CAS ausgeliefert werden kann (Wenn das Projekt als Abhängigkeit eingetragen ist).