Making-of: Der Web Report Designer in der Entwicklung Teil II

Wie bereits im ersten Blogpost beschrieben, wollen wir im zweiten Teil auf die weiteren Herausforderungen bei der Entwicklung des Web Report Designers eingehen. So können Sie von unseren Learnings profitieren und vielleicht sogar das eine oder andere für Ihre eigenen Projekte weiterverwenden.

Der Web Report Designer selbst ist eine in React geschriebene Web Component. Um in einer UI eine Komponenten-Architektur umzusetzen, sind React-Komponenten ein sehr guter Weg. Hierzu muss man allerdings einige der gelernten Dinge ignorieren, wie z.B. die Trennung von Javascript und Markup Code. React-Komponenten bestehen aus Javascript Code, HTML und manchmal auch aus CSS. Diese sind in sich abgeschlossen und lassen sich über Properties steuern. Da eine React-Komponente bei der klassischen MVC-Denkweise nur den View Part abdeckt, stellt sich hier die Frage, wie der globale State verwaltet werden soll. Dabei sollen auf der Javascript-Clientseite die Zustände der Objekte möglichst einfach gehalten werden. Daher wurde eine komponentenübergreifende „State-Verwaltung“ gesucht. 

Hierbei gab es verschiedene Ansätze bzw. Möglichkeiten, die wir evaluiert haben:

  1. Verwaltung über den localStorage des Browsers
  2. Verwaltung über ein globales Javascript Objekt
  3. MobX, welches das Observer Pattern umsetzt
  4. Redux, welches das Flux Pattern implementiert

localStorage

Diese Variante der Implementierung wurde sehr schnell wieder verworfen, da die States dann persistent auf dem Client gespeichert wären und das unserem Implementierungsansatz widersprechen würde.

Globales Javascript Objekt

Durch die Komplexität der einzelnen Objekte wie z.B. Datenquellen, Listen, Objekte, Variablen oder auch Funktionen, die List & Label verwendet, wurde entschieden, dass dieser Lösungsansatz ebenfalls verworfen werden muss.

MobX

MobX liegt ein mächtiges Konzept zu Grunde nämlich die konsequente Umsetzung des „Observable“ Patterns, wenn es um den Umgang mit State geht.

Hierbei werden Klassen in Javascript angelegt und über Decorators oder Hooks als „observable“ markiert. Sollte sich ein Wert innerhalb dieser Klasse ändern, wird automatisch ein Callback aufgerufen, um auf diese Änderung zu reagieren.

Weiterführende Informationen und ein kleines Bespiel erhalten Sie auf der MobX Homepage.

Allerdings wurde diese Variante ebenfalls verworfen, da bereits im Report Server Redux bzw. das Redux Toolkit mit Erfolg eingesetzt wurde und wir hier auf keine neue bzw. andere Technologie setzen wollten.

Redux

Bei Redux bzw. der Flux Architektur handelt es sich um eine Architektur die erstmalig 2013 von Facebook in Zusammenhang mit React verwendet wurde. 

Facebook hat sich hier zwei wichtige Konzepte überlegt:

  1. Es darf nur eine „Quelle der Wahrheit“ geben – wichtig hierbei ist, dass der globale State nicht mit dem internen State einer Komponente verwechselt wird.
  2. Änderungen am Store sollten niemals direkt geschehen, sondern immer über sogenannte „Actions“.

Aus diesen Konzepten ergibt sich dann die Flux-Architektur:

Actions
Actions sind ganz normale Javascript Objekte, die mindestens aus einem „type“ Attribute bestehen. Dadurch können Stores erkennen, ob es sich um eine relevante Action für Sie handelt.

Dispatcher
Ein Dispatcher nimmt eine Action entgegen und verteilt diese an die Stores.

Stores
In den Stores wird die eigentliche Applikationslogik verwaltet. Stores hören auf die Actions aus dem Dispatcher. Wird eine Action erkannt, die für den Store relevant ist, so kann diese den Store verändern. Anschließend benachrichtigt der Store die React Komponenten über die Änderung und diese wird für den Benutzer sichtbar.

View
Bei der View handelt es sich um den sichtbaren Teil der Anwendung für den Benutzer, welcher wiederum per Action/Dispatcher die verschiedenen Aktionen auslöst.

Redux Toolkit

Da der Aufbau von Redux verhältnismäßig komplex ist wurde für die einfachere Verwaltung das „Redux Toolkit“ verwendet. Bei „Redux Toolkit“ kann durch Verwendung der Methode „createSlice“ schnell und einfach ein weiterer Store-Bestandteil erstellt werden – ohne den großen Overhead welcher sich normalerweise durch Redux ergibt. Nachfolgendend sehen Sie die komplette Implementation des „Hotkey“-Stores:

import { createSlice, PayloadAction } from "@reduxjs/toolkit"; export interface OverwriteBaseHotkeysObject { OverwriteBaseHotkeys: boolean, NewProviderId: string, } export interface HotkeysState { OverwriteBaseHotkeys: boolean, CurrentCustomProvider: string, } const initialState: HotkeysState = { OverwriteBaseHotkeys: false, CurrentCustomProvider: '', } const HotkeysSlice = createSlice( { name: 'Hotkeys', initialState, reducers: { setOverwriteBaseHotkeys(state, { payload }: PayloadAction<OverwriteBaseHotkeysObject>) { state.OverwriteBaseHotkeys = payload.OverwriteBaseHotkeys; state.CurrentCustomProvider = payload.NewProviderId; }, resetHotkeys(state) { state.OverwriteBaseHotkeys = initialState.OverwriteBaseHotkeys; state.CurrentCustomProvider = initialState.CurrentCustomProvider; } } }); export const { setOverwriteBaseHotkeys, resetHotkeys } = HotkeysSlice.actions; export const HotkeysReducer = HotkeysSlice.reducer;
Code language: JavaScript (javascript)

Hierbei werden zuerst die interface-Definitionen, d.h. die Variablen des Stores angelegt. Anschließend werden hierfür die initiellen Werte bestimmt. Bei den Reducern handelt es sich um die „Actions“, welche dann später durch den Dispatcher verwendet werden können.

Um einen Einblick in die Komplexität des Web Report Designer Stores zu bekommen nachfolgend ein Screenshot. Der Store wurde im Web Report Designer in mehrere „Unter“-„Stores“ geteilt um die Arbeit damit zu erleichtern. Es gibt zurzeit 8 „Unter“-„Stores“ die verwendet werden:

1. PropertyGridDefinition-Store: Hier werden sämtliche States verwaltet, die mit den Eigenschaften eines Objekts zu tun haben. Diese sind jeweils an ein Objekt im Designer gebunden:

Hier können alle benötigten Informationen zu den einzelnen Eigenschaften eines Objekts abgerufen werden, wie z.B. PropertyName, DisplayName, um welchen Editor es sich handelt, welche Werte für diese Eigenschaft erlaubt sind, die möglichen Werte, wenn es sich um ein Dropdown bzw. eine Liste handelt, aber auch ob für diese Eigenschaft „Formeln“ unterstützt werden.

2. Config-Store: Hier werden alle States verwaltet, die für die Kommunikation für das Backend benötigt werden.

3. UI-Store: Hier werden alle States verwaltet, welche für die UI benötigt werden, wie z.B. ausgewählte Objekte oder Fenster, der Zoom-Faktor oder auch welche Hotkeys aktuell verfügbar sind.

4. Localization-Store: Dieser dient zur Sprachverwaltung für die aktuelle Instanz. Hierzu wird beim Laden des Web Report Designers ein Hashvergleich gestartet, um zu prüfen, ob die aktuellen Ressourcen zur jeweils gewählten Sprache passen. Dieser Store speichert die Informationen zusätzlich im Local Storage, damit diese sessionübergreifend zur Verfügung stehen.

5. SVG-Store: Hier werden alle SVG-Objekte zu den jeweiligen List & Label Objekten gespeichert, damit diese im Layout angezeigt werden können.

6. History-Store: Hier werden die vergangenen bzw. zukünftigen Objekte für das „Rückgängig“ bzw. „Wiederherstellen“ Feature gespeichert. Wird ein Objekt verändert, wird automatisch ein „dispatch“ auf diesen Store ausgelöst, welcher sich dann das aktuelle Objekt speichert.

7. Project-Store: Hierbei handelt es sich um die eigentliche List & Label Projektdatei. Dieser Store spiegelt das Objektmodell einer List & Label Projektdatei wieder und enthält alle Informationen zu den einzelnen Objekten:

8. EvaluationManager-Store: Hier werden alle Formeln, die bereits einmal vom Backend evaluiert wurden, gespeichert, um die Anzahl der Requests auf das Backend zu minimieren.

Im Rahmen der Umsetzung wurde festgestellt, dass aufgrund der Komplexität des Stores der klassische Zugriff über die Hook „useSelector(…)“ extrem kompliziert ist, da man hierbei den gesamten Pfad im Store kennen muss. Um z.B. auf ein einzelnes Objekt zuzugreifen, müsste man den Pfad „state.project.Objects“ kennen und anschließend das Array filtern. Um dies zu umgehen, wurde entschieden, dass man für die verschiedenen Store-Bestandteile eigene Custom-Hooks schreibt.

Um auf die Objekte zuzugreifen, wurde z.B. die Hook „useObjects“ geschrieben:

export const useObjects = (): ObjectsState => { const { PrefetchedObjects, ObjectsList, ShowDeleteConfirmation, ObjectError, DeleteObjectError, ObjectsFetching, AssignLayerToObjectsError } = useSelector((state: ApplicationState) => state.project.Objects); return { PrefetchedObjects, ObjectsList, ShowDeleteConfirmation, ObjectError, DeleteObjectError, ObjectsFetching, AssignLayerToObjectsError }; };
Code language: JavaScript (javascript)

Um auf die verschiedenen Variablen und Berichtsparameter zuzugreifen, wurden ebenfalls diverse Hooks geschrieben:

export const useSumVariables = (): SumVariablesState => { const { SumVariablesList, SumVariablesError } = useSelector((state: ApplicationState) => state.project.SumVariables); return { SumVariablesList, SumVariablesError }; }; export const useUserVariables = (): UserVariablesState => { const { UserVariablesList, UserVariablesError } = useSelector((state: ApplicationState) => state.project.UserVariables); return { UserVariablesList, UserVariablesError }; }; export const useReportParameters = (): ReportParametersState => { const { ReportParametersList, ReportParametersError, InitialReportParameterList } = useSelector((state: ApplicationState) => state.project.ReportParameters); return { ReportParametersList, ReportParametersError, InitialReportParameterList }; }; export const useVariablesFields = (): VariablesFieldsState => { const { VariablesFieldsFetched, VariablesFieldsList, VariablesFieldsLastID, VariablesFieldsError } = useSelector((state: ApplicationState) => state.project.VariablesFields); return { VariablesFieldsFetched, VariablesFieldsList, VariablesFieldsLastID, VariablesFieldsError }; };
Code language: JavaScript (javascript)

Hierdurch wurde der Code des Web Report Designers deutlich übersichtlicher – zudem müssen sich die Entwickler nur noch die einzelnen Hooks merken, wenn diese auf ein Store Objekt zugreifen möchten. Diese Verbesserung des Store-Managements führte zu einer deutlichen Zeitersparnis.

Drag & Drop

Bei der Realisierung der Drag & Drop Funktionen im Web Report Designer wurde bereits am Anfang eine Bedarfsanalyse durchgeführt. Hierbei wurden folgende Anforderungen definiert:

  • Sowohl Desktop-Browser als auch mobile Endgeräte müssen unterstützt werden
  • Gleiches „Look and Feel“ wie in der Windows-Desktopversion
  • Unterstützung von Drop-Zonen für spätere Features
  • Objekte müssen größenveränderbar sein

Die eigene Implementierung dieser Funktionen wurde aufgrund der Komplexität bereits sehr früh verworfen. Stattdessen wurde nach einem passenden Package gesucht, das unsere Anforderungen erfüllt. Das Package „react-dnd“ unterstützte unsere Anforderungen besonders gut, da dieses bereits Zusatz-Packages für mobile Endgeräte besitzt. Um die Größenveränderbarkeit zu unterstützen, wurde auf das NPM Package „react-resizable“ gesetzt.

Durch diese 2 NPM Packages wird im Web Report Designer der komplette „Design Space“ verwaltet. Hierzu wurde zunächst eine Basis-Komponente erstellt, in welche alle Objekte aus dem „Design Space“ gerendert und durch Callbacks verändert werden können:

... return ( <React.Fragment> <Rnd ref={rndRef} key={Item.InternalId} style={{ position: 'absolute', left: "auto !important", top: "auto !important", overflow: "hidden", outline: onDragState ? `${themeContext.designSpace.DesignSpaceObjectDraggedOutlineStyle}` : ActiveObject !== undefined && Item.InternalId === ActiveObject.ID ? `${themeContext.designSpace.DesignSpaceObjectActiveOutlineStyle}` : "", cursor: (SVGObject?.ObjectSVGError?.HasError || actionError || showReportContainerAsTable) ? 'default' : ActiveObject !== undefined && Item.InternalId !== ActiveObject.ID ? 'pointer' : (Prefetched ? 'pointer' : 'move'), zIndex: ActiveObject !== undefined && Item.InternalId === ActiveObject.ID ? 99 : 2, }} default={{ x: left, y: top, width: width, height: height, }} position={{ x: left, y: top }} size={{ width: width, height: height, }} // grid settings raster dragGrid={!canRepositionObject ? [0, 0] : (ProjectOptions?.Layout.Grid.Snap === "True" ? [gridSpacingHorizontal, gridSpacingVertical] : [1, 1])} resizeGrid={ProjectOptions?.Layout.Grid.Snap === "True" ? [gridSpacingHorizontal, gridSpacingVertical] : [1, 1]} onClick={onClick} onDrag={!onResizeState ? onDrag : undefined} onDragStop={!onResizeState ? onDragStop : undefined} onDragStart={!onResizeState ? onDragStart : undefined} dragHandleClassName={`dragHandle_${Item.InternalId}`} onResize={onResize} onResizeStop={onResizeStop} disableDragging={Prefetched || (ActiveObject !== undefined && Item.InternalId !== ActiveObject.ID) || !canRepositionObject || SVGObject?.ObjectSVGError?.HasError || actionError || showReportContainerAsTable} enableResizing={!Prefetched && ActiveObject !== undefined && Item.InternalId === ActiveObject.ID && !SVGObject?.ObjectSVGError?.HasError && !actionError} > ...
Code language: JavaScript (javascript)

Unsere weitere Vorgehensweise und die Herausforderungen die dabei aufgetreten sind – vor allem in Hinblick auf „Back-End-Anfragen“, „Hotkeys“ und „Editor“, erfahren Sie in Kürze im letzten Teil.

Empfohlene Artikel

Schreibe einen Kommentar