• Keine Ergebnisse gefunden

6.1 Backend

6.2.2 Controller und Benutzeroberfläche

Die Anwendungs- und Darstellungslogik sind bei Angular stark verbunden. Jede Komponente besteht aus einer TypeScript-Datei, welche die Funktionalität eines Controllers übernimmt und einer HTML-Datei, welche in Verbindung mit einer SCSS-Datei für die Darstellung der Daten verantwortlich ist.

Die Benutzeroberfläche gliedert sich jeweils in zwei Komponenten. Zum einen die Übersichts-liste, welche alle existierenden Datensätze eines Typs darstellt und einem Dialog, welcher für das Erstellen und Bearbeiten zuständig ist.

Beide Komponenten wurden für Equipment, Material und Vehicles so gestaltet, dass diese vom Polymorphie-Ansatz profitieren können. Am deutlichsten ist dies beim Dialog zu erkennen. Vie-le Felder und der grundVie-legende Ablauf sind bei alVie-len drei Arten identisch. Daher kann derselbe Dialog genutzt werden und es werden einige Felder ausgeblendet, wenn diese nicht benötigt werden. In der weiteren Programmlogik muss dann noch darauf geachtet werden, dass immer der richtige Service zur Kommunikation mit dem Backend aufgerufen wird.

Als Beispiel wird hier das Führerscheintypen-Feld von Fahrzeugen genutzt, da dieses den An-satz gut abbildet.

Darstellung Angular erlaubt das Ausführen simpler Befehle im HTML-Code. So ist es ein-fach möglich Daten kontextabhängig anzuzeigen. In Zeile 2 des Codeausschnittes 6 wird mit einem*ngIfdas Führerschein-Feld nur dargestellt, wenn der Dialog im Fahrzeug-Modus ge-öffnet ist.

Im weiteren Code werden einige Texte gesetzt und Events entsprechende Funktionen im TypeScript-Code zugewiesen.

Interessant ist hier noch dasmat-autocomplete. Über ein*ngFor wird für jeden Eintrag in derfilteredLicense-Variable eine Zeile im Autocomplete-Feld erzeugt. Für den Text wird die Bezeichnung des jeweiligen Führerscheintyps genutzt. Als Wert, welcher für das Klick-Event auf einen Eintrag in der Liste genutzt wird, wird die jeweilige Id genutzt. Dies ermöglicht später eine einfache und eindeutige Identifizierung eines angeklickten Eintrages.

1 <mat-form-field

2 *ngIf="resourceType === dialogResourceTypes.vehicle"

3 [ngClass]="'inputFullWidth'">

4 <input type="text"

9 [matAutocomplete]="autoLicense">

19 <mat-option *ngFor="let license of filteredLicenses | async"

20 [value]=license.id>

21 {{license.type}}

22 </mat-option>

23 </mat-autocomplete>

24 </mat-form-field>

Quellcode 6: Autocomplete-Feld für Führerscheintypen

Controller Die TypeScript-Datei stellt die Funktionen bereit, welche die Oberfläche mit Da-ten versorgt und behandelt von ihr ausgelöste Events.

Jedes Formelement in der Benutzeroberfläche erhält eineFormControl[BD+19a]. Diese ver-waltet den aktuellen Wert der einzelnen Formelemente und kann diese mittels Validatoren auf ihre Gültigkeit prüfen [BD+19a]. Zur Strukturierung der FormControls werden diese in Form-Groups zusammengefasst [BD+19b].

Abbildung 6: Struktureller Aufbau des Dialogs Der Ressourcendialog ist in eine

Haupt-und zwei Subkomponenten aufgeteilt.

Die Hauptkomponente beinhaltet den Dialog selbst sowie die, nur beim Er-stellen einer neuen Ressource benötigte, erste Dialogseite zur Auswahl des Res-sourcentyps. Die beiden Subkomponen-ten beinhalSubkomponen-ten jeweils eine Seite für die

verpflichtenden sowie optionalen Felder. Dies ist schematisch in Abbildung 6 zu sehen. Da die Subkomponenten nur für das Ausfüllen der Felder zuständig sind, müssen die Werte in der Hauptkomponente verfügbar sein, sodass diese dort weiterverarbeitet werden können. Um dieses Problem zu lösen werden die FormGroups für die Subkomponenten in der Hauptkompo-nente initialisiert.

Die Initialisierung einer FormGroup ist im Quellcode 7 zu sehen. Der FormGroup werden im Konstruktor beliebig viele FormControls übergeben. Jede FormControl erhält einen Namen,

über den sie später referenziert werden kann. Die FormControl idwird hier direkt mit einem initialen FormState und einer leeren Liste von Validatoren initialisiert. Die weiteren FormCon-trols in dieser Group können hier nicht mit Validatoren initialisiert werden, da nicht jedes Feld immer sichtbar ist. Das Führerschein-Feld ist beispielsweise nur imVehicle-Modus sichtbar. Für einen Validator ist es aber nicht relevant, ob das Feld sichtbar ist, da dieser immer ausgeführt wird. Somit dürfen nur die sichtbaren Felder Validatoren gesetzt haben. Ein weiteres Problem sind Validatoren, welche auf Daten angewiesen sind. Für die Führerscheine muss zuerst die vollständige Liste aller angelegten Führerscheintypen vorhanden sein.

1 public formRequired: FormGroup;

2 constructor() {

3 this.formRequired = new FormGroup({

4 id: new FormControl(-1, []),

5 license: new FormControl(),

6 });

7 }

Quellcode 7: Erstellen einer FormGroup

Die in der Hauptkomponente angelegten FormGroups werden also an die Subkomponenten übergeben. Dort können dann, wie im Code-Ausschnitt 8 zu sehen ist, die Validatoren gesetzt werden. Dies ist immer notwendig, wenn sich derresouceTypeändert. Da der Ressourcentyp nur über die Hauptkomponente geändert werden kann und dann in die Subkomponenten wei-tergereicht wird, ist es ausreichend die Lifecycle-MethodengOnChangeszu nutzen. Dort wird geprüft, ob sich der Typ geändert hat, oder ob das Update aus einem anderen Grund ausge-löst wurde. Hat sich der Typ geändert wird die MethodesetTypeDependentValidatorszum Erneuern der Validatoren aufgerufen.

Dort werden zuerst alle bereits gesetzten Validatoren entfernt. Im nächsten Schritt wird geprüft, ob Validatoren für das Führerschein-Feld gesetzt werden müssen. Ist dies der Fall wird die Lis-te der Führerscheine vom Service abgerufen. So ist sichergesLis-tellt, dass die LisLis-te der erlaubLis-ten Werte für den Validator gültig und aktuell ist. In der Callback-Funktion, welche gerufen wird sobald die Antwort vom Service angekommen ist, werden die Validatoren für das Feld gesetzt.

Außerdem wird auch direkt die Vorschlagsliste für das Autocomplete Feld in der Benutzerober-fläche gesetzt. Abschließend wird noch die FunktionupdateValueAndValidity()für jedes aktualisierte FormControl aufgerufen. Dies löst eine sofortige Aktualisierung des Validation-Status aus. Diese Methode muss immer nach dem Ändern von Validatoren ausgeführt werden, auch wenn die Liste der Validatoren geleert wird. Daher findet sich diese Zeile sowohl am Ende der MethodesetTypeDependentValidators()und am Ende der Callback-Funktion.

1 this.setTypeDependentValidators();

2 }

3 }

4 private setTypeDependentValidators() {

5 this.formRequired.controls.license.setValidators([]);

6 if (this.resourceType === DialogResourceTypes.vehicle){

7 this.licenseService.all.then(() => {

21 public changeLicenseInputHandler(event: string | number) {

22 if (typeof event === 'string') {

23 const hit = this.licenseService.cache.find(l => l.type === event);

24 if (hit) {

25 this.formRequired.controls.license.setValue(event);

26 }

27 }

28 }

29 public displaySelectedLicense(id: number): string {

30 if (!id) {return '';}

31 return this.licenseService.cache.find(l => l.id === id).type;

32 }

33 private _filterLicenses(value: string | number): ApiLicenseTypeModel[] {

34 if (!value) {value = '';}

35 if (this.licenseService.cache.length === 0) {

36 return [ApiLicenseTypeModel.emptyObject()];

37 }

38 if (typeof value === 'number') {

39 return this.licenseService.cache.filter(l => l.id === value);

40 }

41 return this.licenseService.cache.filter(l =>

l.type.toLowerCase().includes(value.toLowerCase()));

,→

42 }

Quellcode 8: Vereinfachte, relevante Funktionen für Führerscheintypen aus der Komponen-te für Pflichtfelder

Die Methode _filterLicenses(value) liefert eine Liste von Führerscheintypen zurück, welche dem Filter entsprechen. Zuerst werden der Parametervalue und die Führerscheinliste auf ihre Gültigkeit geprüft. Im Anschluss wird die Liste nach dem Kriterium des Parameters value gefiltert und zurückgeliefert. Diese Funktion wird bei jeder Änderung des Wertes im Führerschein-Feld aufgerufen. Als Parameter wird der aktuelle Inhalt des Eingabefeldes über-geben. So wird der Benutzeroberfläche eine, zur aktuellen Eingabe passende, Liste mit den Führerscheintypen bereitgestellt.

Nun bleiben noch die in Codeausschnitt 6 genutzten Methoden changeLicenseInputHandler($event) und displaySelectedLicense.bind(this).

Beide Methoden sind im TypeScript-Teil der Subkomponente zu finden. Da in der Form-Control als Wert die Id des jeweiligen Eintrages gespeichert wird, muss diese in einen passenden Text für die Anzeige umgewandelt werden. In diesem Fall wird in der Methode displaySelectedLicense(id) der, zur Id passende, Eintrag der Führerscheinliste gesucht und das Feldtypedieses Eintrages zurückgegeben.changeLicenseInputHandler(event) macht effektiv das Gegenteil. Sie stellt sicher, dass in der FormControl nur eine Id und kein String eingetragen wird und dies auch nur, sofern zur aktuellen Eingabe ein gültiger Eintrag gefunden wurde.

6.3 App

Im Gegensatz zur Webanwendung gibt es in der mobilen App noch keine Komponenten, wie eine Menüführung oder einen BaseService. Deshalb müssen viele Komponenten, welche in der Webanwendung bereits vorhanden sind, in der mobilen App erst für dieses Projekt entwickelt werden. Einige der im Zuge dieser Arbeit entwickelten Module werden deshalb auch in anderen Teilen der Hauptanwendung verwendet.

6.3.1 Services

Dieser Teil der App ist weitestgehend identisch zur Webanwendung. Die Services sind, ebenso wie in der Webapp, für die Kommunikation mit dem Backend zuständig und müssen ein funktio-nierendes Caching haben, um kurzzeitige Netzwerkabbrüche überbrücken zu können. Aufgrund dieser Ähnlichkeit wurde das Konzept des BaseServices aus der Webanwendung übernommen und in der App ebenfalls umgesetzt.

BaseService Der BaseService implementiert ein einheitliches Interface für alle Services und bietet ein optionales, intelligentes Caching. Das Caching hält den Datensatz im Cache immer auf dem aktuellen Stand, indem es Post, Put und Delete nicht nur an das Backend sendet, son-dern diese Operation auch im lokalen Cache durchführt. So ist kein neuer Get notwendig, um

den Cache zu aktualisieren. Dieses Verfahren funktioniert nur für Anfragen, welche bei einem Get immer die vollständige Liste aller Daten zurückliefert. Anfragen, welche Parameter für den Request benötigen und je nach Parameter unterschiedliche Daten zurück liefern, lassen sich mit diesem Verfahren nicht cachen. Um diese Anfragen cachen zu können, wäre ein Request ba-siertes Caching notwendig, bei dem die Parameter des Requests zusammen mit der Response zwischengespeichert werden. Da fast alle Endpunkte im Backend den Anforderungen des intel-ligenten Caching-Verfahrens entsprechen, ist diese Einschränkung kein relevantes Problem und die Umsetzung des Request-Cachings ist nicht notwendig.

In Quellcode 9 sind die wichtigsten Funktionen für einen Post-Request zu sehen. Die zentrale Methode dieser Klasse ist doRequest(...). In ihr wird der Request ausgeführt und die Re-sponse verarbeitet. Die MethodeafterPost(...) wird standardmäßig als Teil der Funktion processResultausgeführt und gibt, sofern Caching aktiviert ist, den neuen Stand des Caches zurück. Die Methodepost(item)definiert die, für einen Post-Request notwendigen, Parame-ter für diedoRequest(...)-Methode.

1 BaseService(this.endpoint, {this.disableCache = false}) {}

2

3 @override

4 Future<BaseServiceResponse> post(dynamic item) {

5 item = item as T;

6 return doRequest(

7 () => requestHelper.post(endpoint, sanitizeModel(serialize(item))),

8 (data) {

9 BaseModel newItem = item as BaseModel;

10 newItem.id = data['id'];

11 return afterPost(newItem, []..addAll(_cache ?? []));

12 },

13 successCode: 201,

14 );

15 }

16

17 List<BaseModel> afterPost(BaseModel item, List<BaseModel> oldCache) {

18 if (disableCache) return [item];

19 oldCache.add(item);

20 return oldCache;

21 }

22

23 @protected

24 Future<BaseServiceResponse> doRequest(requestFunc, processResult, {int successCode =

200}) async {

,→

25 Response response;

26 BaseServiceResponse sr;

27 try {

28 response = await requestFunc();

29 sr = response.statusCode == successCode

30 ? BaseServiceResponse<T>(statusCode: response.statusCode)

31 : BaseServiceResponse<T>(hasError: true, statusCode: response.statusCode);

32 } catch (e) {

33 int errorCode = ServiceHelper.evaluateException(e);

34 sr = BaseServiceResponse<T>(hasError: true, statusCode: errorCode);

35 }

36

37 List<BaseModel> curData;

38 if (sr.hasError) {

39 curData = disableCache ? [] : cache;

40 } else {

41 curData = processResult(response.data);

42 }

43 if (!disableCache) _cache = curData;

44 sr.data = curData.cast<T>();

45 return sr;

46 }

Quellcode 9: Ausschnitt aus der BaseService-Klasse

Konstruktor Der Konstruktor nimmt den Endpunkt des aktuellen Services entgegen. Au-ßerdem erlaubt der ParameterdisableCachedas Deaktivieren des Caches.

doRequest Neben dem Ausführen des Requests ist diese Methode insbesondere für das Befüllen derBaseServiceResponseverantwortlich. Die BaseServiceResponse beinhaltet ne-ben dem eigentlichen Datensatz auch verschiedene Metainformationen. Dies sind ein Status-Code, welcher entweder einem HTTP-Statuscode entspricht oder im Falle eines Fehlers, wel-cher das erfolgreiche Ausführen des Requests verhindert, ein selbst definierter Statuscode. Das können beispielsweise -1003 für einen Timeout oder -101 für ein Netzwerkproblem sein. Ent-spricht der Status-Code nicht dem angegebenen successCode, so wirdhasError auf true gesetzt. Das FeldisInQueuewird aktuell nicht genutzt. Für eine spätere Erweiterung der An-wendung ist eine Funktionalität angedacht, welche es ermöglicht, bestimmte Anfragen, wie das Hochladen von Bildern, in einer Queue zu speichern und erst später auszuführen, wenn wieder eine Netzwerkverbindung besteht oder der Nutzer in ein WLAN wechselt. Dafür ist das Feld isInQueuevorgesehen.

DiedoRequest-Methode nimmt drei Parameter entgegen. Der ParameterrequestFuncist eine Funktion, welche den Request an sich ausführt. Sie gibt ein Objekt vom TypResponsezurück.

Nun gibt es drei Möglichkeiten. Konnte der Request erfolgreich durchgeführt werden und der Statuscode entspricht dem angegebenen successCode, so wird derBaseServiceResponse

lediglich der Statuscode zugewiesen. Konnte der Request zwar durchgeführt werden, aber das Backend hat einen anderen Statuscode zurückgegeben, wird die BaseServiceResponsemit dem Statuscode und dem FlaghasError initialisiert. Die dritte Möglichkeit ist, dass ein an-derer Fehler bei der Ausführung des Requests aufgetreten ist und es nie zu einer Antwort des Backends kommt. In diesem Fall wird der Exception-Handler ausgeführt. Dieser übergibt die Exception an eine Hilfsfunktion, welche einen passenden Statuscode zurückliefert. Mit diesem Statuscode und dem FlaghasErrorwird hier dieBaseServiceResponseinitialisiert.

Der Request ist damit abgeschlossen und es muss noch der Cache aktualisiert und das Daten-feld in dem Antwort Objekt ausgefüllt werden. Zuerst wird eine neue Liste angelegt. Ist der Request fehlgeschlagen, wird diese Liste mit dem Cache, falls dieser aktiviert ist, initialisiert.

Im Erfolgsfall werden die erhaltenen Daten an den zweiten Parameter, der processResult-Funktion, übergeben und deren Ergebnis der Liste zugewiesen. Sofern aktiviert wird nun der Cache mit den neuen Daten aktualisiert und der Datensatz im Antwort-Objekt gesetzt.

1 class BaseServiceResponse<T> {

8 BaseServiceResponse({this.hasError = false, this.statusCode,

9 this.data, this.isInQueue = false, this.dataSingular});

10 }

Quellcode 10: BaseServiceResponse

afterPost Diese Funktion erhält den neu erstellten Datensatz und den alten Cache. Ist das Caching aktiviert, wird der neue Datensatz dem Cache hinzugefügt und zurückgegeben. Ande-renfalls wird der Datensatz in ein leeres Array eingefügt und dieses zurückgegeben.

post Diese Methode wird aus anderen Programmteilen aufgerufen, um einen neuen Daten-satz zu erstellen. Sie ist nur ein Wrapper um die doRequest-Methode und ruft diese mit den passenden Parametern auf. Als Parameter nimmt sie den neuen Datensatz entgegen.

Der erste Parameter ist die Request-Funktion. Dazu wird die Post-Methode desRequestHelpers aufgerufen, welche wiederum die Post-Methode von Dio, einen einfachen und mächtigen Http-Client für Dart, aufruft [wen+19]. Da das Backend keine null-Werte erlaubt, werden mit Hilfe der Methode sanitizeModel alle Felder, welche als Wert null vorweisen, aus dem Modell entfernt.

Der zweite Parameter ist dieprocessResult-Funktion. Zuerst wird von dem Datensatz, wel-chenpostals Argument erhalten hat, eine Kopie erstellt und die Id in diesem Objekt gesetzt.

Die Id ist an dieser Stelle bekannt, da sie als Teil der Response vom Backend mitgeliefert wird.

Als letztes wird dieafterPost-Methode aufgerufen, um den neuen Datensatz dem Cache an-zufügen.

Equipment-Service Die konkrete Implementierung eines Service ist sehr einfach, da im Standardfall fast die komplette Funktionalität schon imBaseServiceabgedeckt ist.

Jeder Service ist ein Singleton. Dazu wird in Zeile zwei eine statische Variable mit einem Ob-jekt des Service angelegt. Initialisiert wird es über einen privaten Konstruktor. Da dieser nur innerhalb der Klasse aufgerufen werden kann, wird so verhindert, dass eine zweite Instanz von außen angelegt werden kann. Der private Konstruktor ruft den Konstruktor der BaseService-Klasse auf und initialisiert diese mit dem API-Endpunktequipment. Der Factory-Konstuktor erlaubt es, die instance-Variable zurückzugeben, anstatt eine neue Objektinstanz zu erzeu-gen. So kann auf das Singleton zugegriffen werden, als würde ein neues Objekt erzeugt werden.

Dadurch muss sich später nicht weiter um die korrekte Handhabung des Singleton gekümmert werden.

Außerdem müssen die deserialize(data) und serialize(data) Funktionen implemen-tiert werden. Diese rufen ihrerseits eine Methode im Modell auf, welche ein Modell aus dem Datensatz erstellt, respektive es in eine Map umwandelt.

1 class EquipmentService extends BaseService<Equipment> {

2 static final EquipmentService instance = EquipmentService._();

3

4 factory EquipmentService() => instance;

5

6 EquipmentService._() : super('equipment');

7

8 @override

9 Equipment deserialize(Map<String, dynamic> data) =>

10 Equipment.fromMap(data);

11

12 @override

13 Map<String, dynamic> serialize(Equipment data) => data.toMap();

14 }

Quellcode 11: EquipmentService als Beispiel einer konkreten Service-Implementierung