Plugin (Helper) erstellen - minova-afis/aero.minova.rcp Wiki

Erstellen von neuen Plugins/Helpern

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:

Diese Schritte sind im Folgenden genauer beschrieben.

Erstellen des Projekts

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. Um auf Klassen der Core-WFC-Anwendung zugreifen zu können ist folgender Eintrag wichtig:

<location includeAllPlatforms="false" includeConfigurePhase="true" includeMode="planner" includeSource="true" type="InstallableUnit">
	<repository location="https://minova-afis.github.io/aero.minova.rcp.updatesite/"/>
	<unit id="aero.minova.libs.feature.feature.group" version="0.0.0"/>
	<unit id="aero.minova.rcp.feature.feature.group" version="0.0.0"/>
</location>

Die komplette Target-Platform des Workingtime-Plugins findet sich hier.

In der pom.xml müssen alle Module vermerkt sein, die gebaut werden sollen. Komplette Datei für Workingtime hier.

<modules>
	<module>aero.minova.workingtime.helper</module>
	<module>aero.minova.workingtime.helper.tests</module>
</modules>

Beispiel für ein baubares Plugin-Repository:

projectExample

Prinzipiell soll es ein eigenes Repositorie für jede Erweiterungen geben (siehe auch #749). In diesem sind dann neben den Plugins für die WFC-Anwendung auch die Masken, OPs, Übersetzungen, Prozeduren, …​ enthalten. Als Beispiel kann man aero.minova.workingtime anschauen.

Wenn das gesamte Repository in Eclipse geladen werden soll muss auch der Rootordner selbst eine .project Datei enthalten. Diese kann etwa so aussehen:

<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
	<name>aero.minova.workingtime.root</name>
	<comment></comment>
	<projects>
	</projects>
	<buildSpec>
		<buildCommand>
			<name>org.eclipse.jdt.core.javabuilder</name>
			<arguments>
			</arguments>
		</buildCommand>
		<buildCommand>
			<name>org.eclipse.m2e.core.maven2Builder</name>
			<arguments>
			</arguments>
		</buildCommand>
	</buildSpec>
	<natures>
		<nature>org.eclipse.jdt.core.javanature</nature>
		<nature>org.eclipse.m2e.core.maven2Nature</nature>
	</natures>
</projectDescription>

Erstellen des Plugins

In dem client-Ordner des Repositories wird ein neues Plug-In Project erstellt.

pluginErstellen

Das Namesschema ist aero.minova.<Klassenname>.helper.

pluginDialog1 pluginDialog2

MANIFEST.MF anpassen

Das Manifest des Plugins muss angepasst werden und sollte dann etwa so aussehen:

pluginManifest
  • Die Version wird zu Beginn auf 12.0.0 gesetzt. Bei neuen Releases wir die Versionsnummer dann nach üblichem Schema erhöht.

  • Der Name wird sinnvoll gefüllt

  • 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

  • Java-11 wird als Execution Environment eingetragen. Außerdem muss auch "Update the classpath settings" geklickt werden! Wir wollen auf Java-17 upgraden sobald verfügbar, wenn das geschehen ist sollte natürlich auch in den Plug-Ins Java-17 verwendet werden.

Plugin für Tests

Auch für die Plugins können Tests erstellt werden. Dafür wird ein neues Plugin benötigt, das Namensschema ist aero.minova.<maskenname>.helper.tests. Dieses wird dann als Modul in die pom.xml eingetragen, damit die Tests beim Bauen automatisch ausgeführt werden.

Bei den Test-Plugins sollte das Manifest ebenfalls entsprechend angepasst werden, vor allem auf die korrekte Java-Version muss geachtet werden. Da die Tests aber nicht zur Laufzeit wichtig sind muss die Checkbox "Activate this plug-in when one of its classes is loaded" nicht gesetzt werden.

Erstellen der Klasse

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. ACHTUNG: Aktuell kann nur ein Helper geladen werden, wenn in der Form und in einem Grid ein Helper vermerkt ist wird der des Grids genutzt.

<!-- 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
	}
}

Methoden

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.

Laden zur Laufzeit

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:

pluginAnnotations

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").

Programmieren des neuen Helpers

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)

Umsetzen von Funktionalitäten

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");

Reagieren auf Detail-Aktionen

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.

Belegen von Werten in Feldern

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);
  }
}));

Dirty-Flag

Damit das Dirty-Flag richtig funktioniert müssen die vorbelegten Werte ans WFCDetailCASRequestsUtil geliefert werden. Dies funktioniert in Form einer Tabelle:

// Vorbelegte Werte werden für das DirtyFlag ans WFCDetailCASRequestUtil geliefert
Table table = new Table();
table.setName("WorkingTime");
Row r = new Row();

employee.setValue(employeeValue, false);
table.addColumn(new Column(employee.getName(), employee.getDataType()));
r.addValue(employeeValue);

bookingDate.setValue(new Value(DateUtil.getDate("0")), false);
table.addColumn(new Column(bookingDate.getName(), bookingDate.getDataType()));
r.addValue(bookingDateValue);

t.addRow(r);
WFCDetailCASRequestsUtil casUtil = mPerspective.getContext().get(WFCDetailCASRequestsUtil.class);
casUtil.setSelectedTable(t);

broker.post(Constants.BROKER_CHECKDIRTY, ""); // Check über IEventBroker anstoßen

Belegen von Werten in Grids

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.

WFCDetailCASRequestsUtil casUtil = mPerspective.getContext().get(WFCDetailCASRequestsUtil.class);
casUtil.setGridContent(mDetail.getGrid("InvoicePosition"), newDataTable);

Grids Validieren

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 columnsToValidatesteht. 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!");
		}
	}
}

Reagieren auf Wert-Änderungen

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);
		}
	}
}

GridChangeEvent

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 wird mitgegeben

  • DELETE: Eine Zeile des Grids wurde gelöscht. Der Index den die alte Zeile hatte wird mitgegeben

  • UPDATE: Ein einzelner Wert im Grid wurde geändert. Zeilen- und Spaltenindex sowie der alte und neue Wert wird mitgegeben

Buttons

In Version 11 konnten Buttons ü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);

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
		}
	}
});

Ein-/Ausblenden von Sections

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 auf read-only oder required

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();

Sichtbarkeit von Feldern

Die Sichtbarkeit von Feldern kann über einen Helper geändert werden. Achtung beim unsichtbar-setzten 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

Param-String Felder

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:

WFCDetailCASRequestsUtil casUtil = mPerspective.getContext().get(WFCDetailCASRequestsUtil.class);
casUtil.updateParamStringField((MParamStringField) mDetail.getField("ExecutionParameter"), "SQL2FileParameter.op.xml");

tools.getSQLValue() Methode

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

Filtern von Optionen für Lookup Felder

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"));

SiteParameter anfragen

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");

Ausführen von Handlern (z.B. zum Speichern)

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);

Benachrichtigungen an Nutzer:in

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.

broker.send(Constants.BROKER_SHOWNOTIFICATION, "msg.notification"); // Popup
broker.send(Constants.BROKER_SHOWERRORMESSAGE, "msg.errormessage"); // Fehlermeldung

Maskenspezifische Einstellungen

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 PreferenceTabDescriptorerweitert. 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>

Erstellen eines Wizards

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.

Eintrag in der Maske

Um den Wizard zu öffnen muss er in der Maske vermerkt sein

<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>

Klassen des Wizards

Jeder Wizard besteht aus mindestens drei Klassen.

  1. Der Wizard selbst

  2. Mindestens einer Page

  3. Der FinishAction

Der Wizard

@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 erleichter. 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

Die Pages

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.

Pages mit 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 die onSelect() 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 geupdatet werden!

Pages mit Tabellen
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 die onSelect() 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.

Die FinishAction

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 der Wizard anderweitig abgeschlossen werden. Wichtig ist die Rückgabe.

  • Bei true wird der Wizard automatisch geschlossen

  • Bei false bleibt der Wizard geöffnet

Bauen (und Release) des Plugins

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.

jarExample

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 automatatisch vom CAS ausgeliefert werden kann (Wenn das Projekt als Abhängigkeit eingetragen ist).

⚠️ **GitHub.com Fallback** ⚠️