CUBA Home Accounting



    In diesem Artikel sollen die Funktionen der CUBA- Plattform am Beispiel der Erstellung einer kleinen nützlichen Anwendung erläutert werden .
    CUBA ist für die schnelle Entwicklung von Geschäftsanwendungen in Java gedacht, darüber haben wir bereits mehrere Artikel über Habré geschrieben.

    In der Regel werden auf einer Plattform entweder echte, aber zu große und geschlossene Informationssysteme oder Anwendungen im Stil von „Hello World“ oder künstliche Beispiele wie „Libraries“ auf unserer Website erstellt. Aus diesem Grund habe ich mich vor einiger Zeit entschlossen, zwei Fliegen mit einer Klappe zu schlagen - schreiben Sie eine nützliche Anwendung für mich und veröffentlichen Sie sie als Beispiel für die Nutzung unserer Plattform, da der Themenbereich für alle einfach und verständlich ist.


    Was ist als Ergebnis passiert?


    Kurz gesagt, die Anwendung löst zwei Hauptaufgaben:
    1. Zu jedem Zeitpunkt wird der aktuelle Kontostand für alle Arten von Bargeld angezeigt: Bargeld, Karten, Einzahlungen, Schulden usw.
    2. Erstellt einen Bericht nach Einnahmen- und Ausgabenkategorien, mit dem Sie herausfinden können, wofür das Geld in einem bestimmten Zeitraum ausgegeben wurde oder woher es stammt.

    Im Detail:
    • Verschiedene Arten von Bargeld werden von Konten dargestellt .
    • Operationen sind bei Eingang auf dem Konto, Kosten vom Konto und Überweisung von Geldern zwischen Konten möglich.
    • In der Einnahmen- oder Ausgabenbuchung können Sie eine Kategorie angeben, um anzugeben, woher das Geld stammt oder wofür es ausgegeben wurde.
    • Der Saldo aller Konten für das aktuelle Datum wird nach jeder Transaktion ständig angezeigt und neu berechnet.
    • Der Bericht nach Ertrags- und Aufwandskategorien enthält eine Zusammenfassung von zwei beliebigen Zeiträumen gleichzeitig, um einen schnellen visuellen Vergleich zu ermöglichen. Jede Kategorie kann vom Vergleich ausgeschlossen werden. Für jede Zeile des Berichts können Sie in der Operation "Fehler" machen, um zu sehen, woraus sie besteht.
    • Das System besteht aus drei Webanwendungen, die auf einem Tomcat bereitgestellt werden:
      1. Middleware
      2. Voll ausgestattete Benutzeroberfläche auf CUBA
      3. Reaktionsschnelle Benutzeroberfläche auf Backbone.js + Bootstrap für die einfache Eingabe von Vorgängen auf Mobilgeräten.

      Die Lösung einer so einfachen Aufgabe erscheint etwas überflüssig, aber zum einen wurde die Anwendung eher für Unterrichtszwecke als für die Praxis erstellt, und zum anderen sind nicht viele Ressourcen erforderlich - meine eigene Kopie kann problemlos auf der Amazon EC2-Mikroinstanz ausgeführt werden.

    Einige Screenshots


    Primäre Benutzeroberfläche: Liste der Vorgänge

    Hauptbenutzeroberfläche: Bericht nach Ertrags- / Aufwandskategorien

    Responsive UI: Liste der Operationen

    Reaktionsschnelle Benutzeroberfläche: aktueller Kontostand


    Wie läuft man?


    Der Quellcode des Projekts ist hier: github.com/knstvk/akkount (KK - das sind meine Initialen, mir ist nichts Besseres eingefallen).
    Die Plattform selbst ist nicht kostenlos, aber fünf gleichzeitige Verbindungen in einer kostenlosen Lizenz sind mehr als genug für den Heimgebrauch. Wenn also jemand sie nutzen möchte, bitte.

    Es werden nur JDK 7+ und die festgelegte Umgebungsvariable JAVA_HOME benötigt. Öffnen Sie zum Erstellen die Befehlszeile im Stammverzeichnis des Projekts, und führen Sie
    gradlew setupTomcat deploy

    Gradle aus, das die Internetplattform und andere Bibliotheken herunterlädt. Anschließend wird die Anwendung im Unterverzeichnis build / tomcat erstellt. Während des Assemblierungsprozesses werden Sie aufgefordert, die Lizenzvereinbarung für die CUBA-Plattform zu akzeptieren.
    Danach müssen Sie den HSQL-Server starten und eine Datenbank im Projektdaten-Unterverzeichnis erstellen:
    gradlew startDb
    gradlew createDb

    Um Tomcat zu starten, können Sie den Befehl Gradle
    gradlew start
    oder Skripte startup.*im Unterverzeichnis verwenden build/tomcat/bin.
    Die Hauptwebschnittstelle der Anwendung ist localhost:8080/appunter responsive UI - at verfügbar localhost:8080/app-portal. Der Benutzer ist admin, das Passwort ist admin.

    Die Datenbank ist anfangs leer, es gibt einen Generator, um sie mit Testdaten zu füllen. Es ist über das Menü Administration -> JMX-Konsole -> app-core.akkount -> app-core.akkount verfügbar: type = SampleDataGenerator . Hier gibt es eine MethodegenerateSampleData(), der eine Ganzzahl als Eingabe verwendet - die Anzahl der Tage seit dem aktuellen Datum, für das Sie Vorgänge erstellen müssen. Geben Sie beispielsweise 200 ein und klicken Sie auf Ausführen. Warten Sie, bis der Vorgang abgeschlossen ist, melden Sie sich dann ab (das Symbol in der oberen rechten Ecke) und melden Sie sich erneut an. Sie sehen ungefähr das gleiche wie in meinen Screenshots.

    Wie man hineinschaut


    Um die Anwendung zu studieren und zu verfeinern, empfehle ich , CUBA Studio, IntelliJ IDEA und das CUBA-Plugin dafür herunterzuladen und zu installieren.

    Außerdem werde ich nicht näher darauf eingehen, wie und was im Studio gemacht wird. Dort ist alles visuell, es gibt kontextbezogene Hilfe, es gibt Videomaterial und Dokumentation auf der Plattform. Ich werde die einzige Nuance bei der Verwendung der HSQL-Datenbank erläutern: Wenn Sie ein Projekt mit HSQL DB öffnen, startet das Studio einen eigenen Server auf Port 9001 und speichert die Datenbanken in einem Verzeichnis ~/.haulmont/studio/hsqldb. Dies bedeutet, dass Sie den HSQL-Server stoppen müssen, wenn Sie ihn separat vom Studio mit Gradle-Befehlen gestartet haben. Datenbankdateien können bei Bedarf einfach von data/akknach übertragen werden ~/.haulmont/studio/hsqldb/akk.

    Im Allgemeinen kann die Anwendung auch auf einer seriöseren Datenbank ausgeführt werden - PostgreSQL, Microsoft SQL Server oder Oracle. Wählen Sie dazu in Studio in den Projekteigenschaften den gewünschten Datenbanktyp aus , und führen Sie Entities -> Generate DB Scripts und anschließend im Hauptmenü Run -> Create database aus .

    Das Hauptziel dieses Artikels ist es, die Entwicklungstechniken auf der Plattform zu zeigen, die in der Benutzeroberfläche von Studio nicht sichtbar sind und die in der Dokumentation nur schwer zu finden sind, wenn Sie nicht im Voraus wissen, wonach Sie suchen sollen. Daher wird die Beschreibung des Projekts fragmentarisch sein und sich auf nicht offensichtliche und nicht standardisierte Dinge konzentrieren.

    Datenmodell




    Entitätsklassen befinden sich in einem Modul global, auf das sowohl die mittlere Ebene als auch die Webclients zugreifen können.

    Hierbei handelt es sich im Grunde genommen um gewöhnliche JPA-Unternehmen, die entsprechend mit Anmerkungen versehen und registriert sind persistence.xml. Die meisten von ihnen haben auch eine CUBA-spezifische Annotation @NamePattern, die den „Instanznamen“ definiert - wie eine bestimmte Entitätsinstanz in der Benutzeroberfläche angezeigt wird toString(). Wenn eine solche Anmerkung nicht angegeben wird, werden nur der toString()Klassenname und die Objektkennung als Instanzname verwendet. Eine weitere spezielle Anmerkung ist @Listeners, dass Klassen von Listenern zum Erstellen / Ändern von Objekten definiert werden. Entity-Listener werden im Folgenden ausführlich beschrieben.

    Zusätzlich zu JPA-Entitäten verfügt das Projekt über eine nicht persistente EntitätCategoryAmount. Instanzen nicht persistenter Entitäten werden nicht in der Datenbank gespeichert, sondern nur zum Übertragen von Daten zwischen Anwendungsebenen und zur Anzeige mit Standard-UI-Komponenten verwendet. In diesem Fall wird mit einer solchen Entität ein Bericht nach Kategorie generiert: Auf der mittleren Ebene werden Daten extrahiert, Instanzen erstellt und CategoryAmountaufgefüllt, und im Webclient werden diese Instanzen in Datenquellen platziert und in Tabellen angezeigt. Standardkomponenten Tablewissen nichts über die Herkunft von Entitäten - für sie sind es nur Objekte, die in den Metadaten der Anwendung bekannt sind. Um eine nicht persistente Entität in die Metadaten aufzunehmen, müssen Sie ihrer Klasse eine Anmerkung und ihren @MetaClassAttributen eine Anmerkung hinzufügen @MetaPropertyund die Klasse in einer Datei registrierenmetadata.xml. Persistente Entitäten werden natürlich auch in Metadaten beschrieben - dazu analysiert der Metadaten-Loader beim Start der Anwendung auch die Datei persistence.xml.

    Neben Entitäten befinden sich beispielsweise Aufzählungsklassen OperationType. Aufzählungen, die im Datenmodell in Entitätsattributen verwendet werden, sind nicht ganz normal: Sie implementieren eine Schnittstelle EnumClassund haben ein Feld id. Somit wird der in der Datenbank gespeicherte Wert vom Java-Wert getrennt. Dies ermöglicht es, die Kompatibilität mit Daten in der Produktionsdatenbank während des willkürlichen Umgestaltens des Anwendungscodes sicherzustellen.

    In Dateien messages.propertiesundmessages_ru.propertiesEin Entitätspaket enthält lokalisierte Namen von Entitäten und deren Attribute. Diese Namen werden in der Benutzeroberfläche verwendet, wenn die visuellen Komponenten sie nicht auf ihrer Ebene neu definieren. Nachrichtendateien sind übliche UTF-8-codierte Schlüsselwertmengen. Das Suchen nach einer Nachricht für ein bestimmtes Gebietsschema ähnelt den Regeln PropertyResourceBundle: Zuerst wird der Schlüssel in Dateien mit einem Suffix gesucht, das dem Gebietsschema entspricht, wenn es nicht gefunden wird, in Dateien ohne Suffix.

    Betrachten Sie die Essenz des Modells.
    • Currency- Währung. Es hat einen eindeutigen Code und einen beliebigen Namen. Die Eindeutigkeit des Währungscodes wird durch einen eindeutigen Index unterstützt, den das Studio in die Skripts zur Datenbankerstellung einbezieht, wenn die Anmerkung @Columneine Eigenschaft enthält unique = true. Die Plattform enthält einen Ausnahmehandler, der ausgelöst wird, wenn die Eindeutigkeit in der Datenbank verletzt wird. Dieser Handler gibt dem Benutzer eine Standardnachricht. Der Handler kann in Ihrem Projekt ersetzt werden.
    • Account- Punktzahl. Es hat einen eindeutigen Namen und eine beliebige Beschreibung. Es enthält auch einen Link zur Währung und ein separates Währungscodefeld. Dieses Feld ist ein Beispiel für eine Denormalisierung zur Verbesserung der Leistung. Da die Kontenlisten normalerweise zusammen mit dem Währungscode angezeigt werden, ist es sinnvoll, Join-Anfragen an die Datenbank zu löschen, indem der Währungscode dem Konto selbst hinzugefügt wird. Wir werden den Entity-Listener zwingen, den Währungscode im Konto zu aktualisieren, wenn die Währung des Kontos geändert wird (obwohl dies in der Praxis äußerst selten ist) - dazu später mehr. Das Konto enthält auch ein Attribut active- ein Zeichen, dass es für die Verwendung in neuen Vorgängen verfügbar ist, und ein Attribut includeInTotal- ein Zeichen, dass der Saldo dieses Kontos in den Gesamtsaldo einbezogen werden soll.
    • Category- Kategorie der Einnahmen oder Ausgaben. Es hat einen eindeutigen Namen und eine beliebige Beschreibung. Attribut catType- Kategorietyp, bestimmt durch Aufzählung CategoryType. Wie oben erläutert, wird der durch den Aufzählungsbezeichner (in diesem Fall die Zeichenfolge "E" oder "I") definierte Wert im Klassenfeld und in der Datenbank gespeichert, und der Getter und Setter und damit der gesamte Anwendungscode arbeiten mit CategoryType.INCOMEund CategoryType.EXPENSE.
    • Operation- Betrieb. Vorgangsattribute: Typ (Überweisung OperationType), Datum, Aufwands- und Ertragskonto ( acc1, acc2) und entsprechende Beträge ( amount1, amount2), Kategorie und Kommentare.
    • Balance- Kontostand zu einem bestimmten Zeitpunkt. Im Allgemeinen wäre es für die Buchhaltung zu Hause durchaus möglich, auf diese Essenz zu verzichten und den Saldo immer „von Anfang an“ dynamisch zu berechnen: Addieren Sie einfach das gesamte Einkommen und nehmen Sie alle Ausgaben auf dem Konto weg. Aus Spaß habe ich mich entschlossen, die Implementierung bei einer großen Anzahl von Vorgängen zu verkomplizieren - der Kontostand zu Beginn eines jeden Monats wird in Kopien gespeichert Balance, und bei der Aufzeichnung jedes Vorgangs werden die Salden zu Beginn des nächsten Monats (und später, falls vorhanden) nachgezählt. Um jedoch den Saldo für das aktuelle Datum zu berechnen, müssen Sie den Saldo nur zu Beginn des Monats erfassen und den Umsatz für den Betrieb des aktuellen Monats berechnen. Dieser Ansatz verursacht im Laufe der Zeit keine Leistungsprobleme.
    • UserData- Schlüsselwertspeicherung einiger benutzerbezogener Daten. Beispiel: Zuletzt verwendetes Konto, Berichtsparameter nach Kategorie. Das heißt, es speichert, was während wiederholter Benutzeraktionen "erinnert" werden muss. Mögliche Schlüssel werden durch Konstanten in der Klasse angegeben UserDataKeys.


    Entity Listener




    Wenn Sie mit JPA gearbeitet haben, haben Sie wahrscheinlich auch Entity-Listener verwendet. Dies ist ein praktischer Mechanismus zum Ausführen von Aktionen, wenn Änderungen an Entitäten in der Datenbank gespeichert werden. Am wichtigsten ist, dass alle von den Listenern vorgenommenen Änderungen in derselben Transaktion vorgenommen werden - ähnlich wie bei Datenbank-Triggern. Daher ist es praktisch, eine Logik zu organisieren, um die Konsistenz eines Datenmodells für Listener aufrechtzuerhalten.

    Entity Listener in CUBA unterscheiden sich in der Implementierung geringfügig von JPA. Listener - Klasse implementieren muss eine oder mehrere spezielle Schnittstellen ( BeforeInsertEntityListener, BeforeUpdateEntityListeneret al.). Listener werden in der Annotation für die Entitätsklasse registriert@ListenersAuflisten von Klassennamen in einem Array von Zeichenfolgen. Sie können Literale der Listener-Klassen nicht direkt in der Entitätsklasse verwenden, da die Entität ein globales Objekt ist, auf das sowohl die mittlere Ebene als auch die Clients zugreifen können, und der Listener nur ein Objekt der mittleren Ebene ist, auf das Clients keinen Zugriff haben. Die Listener leben nur auf der mittleren Ebene, weil sie Zugriff auf EntityManagerandere Möglichkeiten zum Arbeiten mit der Datenbank benötigen .

    In dieser Anwendung führen Entity-Listener zwei Funktionen aus: Erstens aktualisieren sie denormalisierte Felder und zweitens berechnen sie den Kontostand zu Beginn des Monats neu.
    Das erste Problem ist trivial: der Hörer AccountEntityListenerin den Methoden onBeforeInsert(), onBeforeUpdate()aktualisiert den Wert der Währung. Dazu genügt es, auf die zugehörige Instanz zu verweisen Currency.
    Die zweite Aufgabe ist im Wesentlichen eine der Hauptaufgaben in der Geschäftslogik der Anwendung. Tut es OperationEntityListenerin der Art und Weise onBeforeInsert(), onBeforeUpdate(), onBeforeDelete(). Dieser Listener zählt nicht nur den Kontostand neu auf, sondern merkt sich auch die UserDatazuletzt verwendeten Konten in den Objekten .

    Es ist zu beachten, dass es im Before-Listener keine Einschränkungen für die Verwendung EntityManager, das Laden und das Ändern von Instanzen von Entitäten gibt. Zum Beispiel in addOperation()der Verwendung von Querygeladenen und modifizierten Kopien Balance. Sie werden gleichzeitig mit dem Vorgang in einer Transaktion in der Datenbank gespeichert.

    Manchmal ist es im Listener erforderlich, den „vorherigen“ Status des Objekts abzurufen, das sich jetzt im permanenten Kontext befindet, dh den Status, der sich jetzt in der Datenbank befindet. Zum Beispiel in diesem Fall inonBeforeUpdate()Wir müssen zuerst den vorherigen Wert des Transaktionsbetrags vom Saldo abziehen und dann den neuen Wert addieren. Dazu getOldOperation()wird in der Methode eine neue Transaktion gestartet , indem persistence.createTransaction()in ihrem Kontext eine andere Instanz abgerufen EntityManagerwird und über diese der vorherige Zustand der Operation mit der gleichen Kennung aus der Datenbank geladen wird. Dann ist die neue Transaktion abgeschlossen, ohne dass die aktuelle Transaktion beeinflusst wird, in der unser Listener arbeitet.

    Komponenten der mittleren Schicht




    Die Hauptarbeit zum Laden von Daten auf die Client-Ebene und zum Speichern von benutzerdefinierten Änderungen an der Datenbank wird vom auf der Plattform implementierten Standard-DataService ausgeführt. Dadurch arbeiten Datenquellen von visuellen Komponenten. Dies ist in unserer Anwendung nicht ausreichend, sodass mehrere spezifische Dienste erstellt wurden.

    Erstens UserDataServiceermöglicht es die Arbeit mit dem Schlüsselwertspeicher UserDataund stellt eine typisierte Schnittstelle zum Lesen und Schreiben von Entitätskennungen bereit. Die Service-Schnittstelle befindet sich im Modul, globalda sie für die Client-Ebene zugänglich sein muss. Die Service-Implementierung befindet sich im Kernmodul der Klasse UserDataServiceBean. Sie delegiert Anrufe in den PapierkorbUserDataWorker, in dem der Code konzentriert ist, der die nützliche Arbeit erledigt. Dies geschieht, weil diese Funktionalität auch in OperationEntityListenerder mittleren Ebene erforderlich ist. Der Service bildet die „Middleware-Grenze“ und ist nur für den Aufruf von Client-Blöcken gedacht. Es sollte nicht aus den Komponenten der mittleren Schicht aufgerufen werden, da dies zu einer wiederholten Operation des Interceptors führt, der die Authentifizierung überprüft und Ausnahmen auf besondere Weise behandelt. Um die Ordnung wiederherzustellen, sollten Sie die von außerhalb der Middleware aufgerufenen Services von den von innen aufgerufenen Beans trennen. Zumindest, weil beim Aufruf von außerhalb der Transaktion immer Abwesenheit besteht und beim Aufruf aus dem Middleware-Code die Transaktion bereits geöffnet werden kann.

    Nächster Service -BalanceService. Sie können den Wert des Kontostands an einem beliebigen Datum abrufen. Da diese Funktionalität von beiden Clients in der Benutzeroberfläche und auf der mittleren Ebene (Testdatengenerator) benötigt wird, wird sie auch in einem separaten Bin abgelegt BalanceWorker.

    Und der letzte Dienst ist ReportService. Es ruft die Daten für den Bericht nach Kategorie ab und gibt sie in Form einer Liste von Instanzen einer nicht persistenten Entität zurück CategoryAmount.

    Der Behälter ist auch in der mittleren Schicht implementiertSampleDataGeneratorwelches entworfen ist, um Testdaten zu erzeugen. Für eine solche Funktionalität ist in der Regel keine komplexe Benutzeroberfläche erforderlich. Es reicht aus, einen Aufruf mit der Übertragung einfacher Parameter zu versehen. Manchmal müssen Sie einen Status in Form einer Reihe von Attributen anzeigen. Außerdem arbeitet nur der Administrator damit, nicht die Benutzer des Systems. In diesem Fall ist es praktisch, dem Bean eine JMX-Schnittstelle zuzuweisen und seine Methoden über die im Web-Client integrierte JMX-Konsole oder durch Herstellen einer Verbindung mit einem externen JMX-Tool aufzurufen. In unserem Fall hat das Bean eine Schnittstelle SampleDataGeneratorMBeanund ist im spring.xmlKernmodul registriert .

    Beachten Sie, dass die generateSampleData()Bean- Methode als annotiert wird@Authenticated. Dies bedeutet, dass beim Aufruf dieser Methode eine spezielle Systemanmeldung ausgeführt wird und eine Benutzersitzung im Ausführungsthread vorhanden ist. Es ist in diesem Fall nicht erforderlich , da die Methode erstellt und modifiziert durch EntityManagerEinheiten, die die Installation benötigen , während ihre Eigenschaften behalten createdBy, updatedBy- die Datenelemente geändert. Andererseits removeAllData()erfordert die Methode , die auch über die JMX-Schnittstelle aufgerufen wird, keine Authentifizierung, da sie Daten mithilfe von SQL-Abfragen über löscht QueryRunnerund nirgendwo auf die Benutzersitzung zugreift.

    Im Allgemeinen wird eine obligatorische Überprüfung auf das Vorhandensein einer Benutzersitzung nur am Eingang der mittleren Schicht von der Client-Ebene aus durchgeführt - im Service Interceptor. Überprüfen Sie das Vorhandensein der Sitzung und der Benutzerrechte auf der Middleware-Ebene, oder überprüfen Sie diese nicht - der Anwendungsentwickler entscheidet, aber in einigen Fällen ist das Vorhandensein der Sitzung erforderlich, da der Benutzername in den Attributen der Entitätsüberwachung angegeben werden muss. Außerdem werden Benutzerrechte immer eingecheckt DataWorker- in der Ablage, an die DataServicedie Ausführung von CRUD-Operationen mit Entitäten delegiert wird.

    Hauptanwendungsfenster


    Die Standardfunktion des CUBA-Webclients ist ein verstecktes Feld auf der linken Seite des Anwendungsfensters, in dem normalerweise die sogenannten "Anwendungsordner" und "Suchordner" angezeigt werden. Diese Ordner werden für den schnellen Zugriff auf Informationen verwendet. Durch Klicken auf einen Ordner wird ein bestimmter Bildschirm mit einer Liste von Entitäten und einem angewendeten Filter geöffnet.

    Es erschien mir logisch, Informationen über den aktuellen Kontostand im linken Teil des Hauptfensters anzuzeigen. Also habe ich das Balance-Panel oben im Ordner-Panel eingebettet.


    Dies geschieht wie folgt:
    • Eine FoldersPaneKlasse wird von der Plattform geerbt LeftPanel, Methoden init()und refreshFolders(), in denen die Methode aufgerufen wird , neu definiert createBalancePanel(). Darin wird ein neuer Container erstellt, der mit Daten aus gefüllt BalanceServiceund am oberen Rand des übergeordneten Containers platziert wird.
    • Um LeftPanelanstelle der Standardklasse verwendet zu werden FoldersPane, wird die AppWindowKlasse von der Plattform geerbt AkkAppWindowund die Methode neu definiert createFoldersPane().
    • Um AkkAppWindowanstelle der Standardmethode verwendet zu werden AppWindow, wird die createAppWindow()Klassenmethode neu definiert App. Außerdem wird hier die Methode für den Zugriff auf das neue Bedienfeld definiert. getLeftPanel()Sie wird von den Bildschirmen aus aufgerufen, um den Saldo nach dem Festschreiben oder Löschen von Vorgängen zu aktualisieren.


    Operations-Browser


    Die Bildschirmbeschreibung befindet sich in der Datei operation-browse.xml. Hier ist alles Standard, außer der Verwendung von Formatierungsklassen zur Darstellung von Daten und Beträgen in der Operationstabelle.

    Zur Anzeige des Datums wird eine Plattform verwendet DateFormatter, auf die das Format per Schlüssel aus dem Paket lokalisierter Nachrichten übertragen wird. Daher kann die Formatzeichenfolge für verschiedene Sprachen unterschiedlich sein - für Russisch wird das Datum durch Punkte getrennt, und für Englisch - mit /.
    Damit die Beträge ohne Bruchteil und 0 überhaupt nicht angezeigt werden, wurde im Projekt eine Klasse angelegt DecimalFormatter- sie wird in den Betragsspalten verwendet.

    Betriebseditor


    Hier ist es interessanter: Eine Operation kann eine von drei Arten sein (Einnahmen, Ausgaben, Überweisungen), und der Bearbeitungsbildschirm sollte für sie unterschiedlich aussehen.




    Auf den ersten Blick scheinen die ersten beiden Bildschirme gleich zu sein, aber das ist eigentlich nicht der Fall: Visuelle Komponenten arbeiten mit unterschiedlichen Attributen der Entität Operation- Aufwand mit acc1und amount1, Ertrag mit acc2und amount2. Diese Variabilität konnte vollständig im Controller-Code implementiert werden, aber ich entschied mich dafür, dies deklarativer zu tun - indem ich die verschiedenen Teile des Bildschirms in separate Frames aufteilte.

    Drei Frames - nach der Anzahl der Arten von Operationen. Alle befinden sich im selben Paket wie der Bearbeitungsbildschirm für Vorgänge. Meist werden Frames statisch verbunden - mit der Komponenteiframein einem XML-Screen-Deskriptor. Dies passt nicht zu uns, da wir den gewünschten Rahmen abhängig von der Art der Operation auswählen müssen. Daher wird im XML-Deskriptor des Bildschirms operation-edit.xmlnur der Container für den Frame definiert - die Komponente groupBoxmit dem Bezeichner frameContainer, und die eigentliche Erstellung und Einfügung des Frames in den Bildschirm wird in der Steuerung ausgeführt OperationEdit:
        @Inject
        private GroupBoxLayout frameContainer;
        private OperationFrame operationFrame;
        @Override
        public void init(Map params) {
        ...
                String frameId = operation.getOpType().name().toLowerCase() + "-frame";
                operationFrame = openFrame(frameContainer, frameId, params);
    

    Hier OperationFrameist die Schnittstelle, die die Operationstyp-Frame-Controller implementieren. Dadurch ist es bequem, alle drei Frames einheitlich zu verwalten - zu initialisieren und zu validieren.

    Ein weiterer interessanter Punkt in der init()Controller- Methode OperationEditist, dass ein Listener registriert ist, der nach dem Festschreiben einer Operation ausgelöst wird:
        @Override
        public void init(Map params) {
            ...
            getDsContext().addListener(new DsContext.CommitListenerAdapter() {
                @Override
                public void afterCommit(CommitContext context, Set result) {
                    LeftPanel leftPanel = App.getLeftPanel();
                    if (leftPanel != null)
                            leftPanel.refreshBalance();
                }
            });
        }
    

    Dieser Listener aktualisiert den Inhalt des linken Fensters und zeigt den aktuellen Kontostand an.

    Operationstyp-Frames haben das folgende gemeinsame Merkmal: Textfelder, die mit Summen arbeiten, werden nicht an die Datenquelle angehängt. Dies geschieht, damit Sie einen arithmetischen Ausdruck in das Feld eingeben können und das System den Betrag berechnet.

    Überlegen Sie expense-frame.xml. Es deklariert eine Komponente textFieldmit einem Bezeichner amountField. Die Steuerung ExpenseFrameverwendet einen Bin, AmountCalculatorin dem die Summenberechnungslogik eingekapselt ist:
        @Inject
        private TextField amountField;
        @Inject
        private AmountCalculator amountCalculator;
        @Override
        public void postInit(Operation item) {
                amountCalculator.initAmount(amountField, item.getAmount1());
            …
        }
        @Override
        public void postValidate(ValidationErrors errors) {
                BigDecimal value = amountCalculator.calculateAmount(amountField, errors);
            …
        }
    

    Dieselbe Bean, die auf der Web Client-Ebene definiert wurde, wird auch in zwei anderen Frame-Controllern verwendet. Die initAmount()Bean- Methode legt den aktuellen Betrag fest, der im Textfeld nach Datentyp formatiert ist BigDecimal. Es ist einfach datatype = decimalunmöglich , die Komponente anzugeben , da in diesem Fall nur eine Zahl eingegeben werden kann und arithmetische Ausdrücke eingegeben werden müssen. Die Methode calculateAmount()überprüft den Ausdruck mithilfe von Regexp auf Richtigkeit und führt ihn dann als Ausdruck in Groovy über die Schnittstelle aus Scripting. Das Ergebnis ist eine Nummer, die zum Einrichten des Vorgangs an die Bildschirmsteuerung zurückgegeben wird.

    Kategoriebericht




    Dieser interaktive Bericht wird vom Bildschirm implementiert categories-report.xml. Es ist vor allem deshalb interessant, weil es zwei benutzerdefinierte Datenquellen des Typs enthält CategoryAmountDatasource. Die Datenquellenklasse wird im datasourceClassElementattribut angegeben collectionDatasource. Für diese Datenquellen ist auch ein JPQL-Operator angegeben, der jedoch nicht verwendet wird und nur vorhanden ist, weil das Studio den Abfragetext automatisch generiert, wenn er nicht angegeben ist. Tatsächlich CategoryAmountDatasourceüberschreibt die Datenquelle die Methode loadData()und DataServiceruft den Dienst auf ReportService, anstatt Daten über eine JPQL-Abfrage zu laden , und übergibt ihm die erforderlichen Parameter:
    public class CategoryAmountDatasource extends CollectionDatasourceImpl {
        private ReportService service = AppBeans.get(ReportService.NAME);
        @Override
        protected void loadData(Map params) {
        ...
                Date fromDate = (Date) params.get("from");
                Date toDate = (Date) params.get("to");
            ...
                List list = service.getTurnoverByCategories(fromDate, toDate, categoryType, currency.getCode(), ids);
                for (CategoryAmount categoryAmount : list) {
                        data.put(categoryAmount.getId(), categoryAmount);
                }
            ...
        }
    

    Die Parameter werden von der Bildschirmsteuerung in der refresh()Datenquellenmethode festgelegt - siehe Methoden refreshDs1(), refreshDs2()Klassen CategoriesReport. Der Dienst gibt eine Liste nicht persistenter Entitätsinstanzen zurück CategoryAmountund die Datenquelle speichert sie in ihrer Datensammlung. Daher werden in den diesen Datenquellen zugeordneten Tabellen Instanzen CategoryAmountwie alle anderen Entitäten angezeigt, die auf die übliche Weise aus der Datenbank geladen wurden.

    Die Funktion der Schaltfläche Ausschließen ist interessant gestaltet , sodass Sie die ausgewählte Kategorie aus der Betrachtung entfernen können.

    Im Deskriptor categories-report.xmlsind zwei solche Schaltflächen deklariert - für die linke und rechte Tabelle. Jede der Schaltflächen ist mit einer Aktion verknüpft.excludeCategorydein Tisch. Es wurden jedoch keine Aktionen für Tabellen im XML-Deskriptor deklariert. Wie funktioniert das Tatsache ist, dass in diesem Fall Aktionen für Tabellen in der init()Screen-Controller- Methode hinzugefügt werden : siehe Methode initExcludedCategories(). Diese Methode „ruft“ auch die Liste der zuvor ausgeschlossenen Kategorien auf, die mit dem Dienst gespeichert wurden UserDataService.

    Die Aktion type ExcludeCategoryActionruft beim Auslösen eine Methode auf excludeCategory(), ComponentsFactorydie einen Container und eine Inschrift mit einer der ausgeschlossenen Kategorie entsprechenden Verknüpfungsschaltfläche erstellt und den neuen Container in den Container einfügt, der zuvor im Handle deklariert wurdeexcludedBox. Für jede Schaltfläche wird beim Auslösen ein Listener erstellt. Der gesamte Container, in dem sich die Schaltfläche zusammen mit der Beschriftung befindet, wird aus dem übergeordneten Container entfernt. Darüber hinaus werden Datenquellen durch Reorganisation von Kategorielisten aktualisiert.

    Im Allgemeinen ist der Kategoriereportbildschirm eine nicht standardmäßige Option für die Verwendung der Plattform, daher enthält die Plattform viele manuell geschriebene Logik, die normalerweise in den Standardoptionen für die Komponenteninteraktion verborgen ist.

    Danksagung


    Ich habe einige Ideen vom wundervollen zenmoney.ru- Service erhalten , den ich für einige Zeit genutzt habe. Alle Open-Source-Bibliotheken und Frameworks, die in der Plattform enthalten sind, werden im Fenster Hilfe -> Info -> Credits aufgelistet .

    Fortsetzung folgt


    Im nächsten Artikel über dieselbe Anwendung möchte ich auf die auf Geräteblöcke reagierende Benutzeroberfläche eingehen, die in Backbone.js + Bootstrap geschrieben ist und über die REST-API mit der mittleren Ebene interagiert. Außerdem werde ich versuchen, das Thema der Hauptbenutzeroberfläche leicht zu ändern und es mit einer neuen Benutzeroberflächenkomponente hinzuzufügen, um die Möglichkeiten der Anpassung der Benutzeroberfläche in Projekten zu veranschaulichen.

    Jetzt auch beliebt: