Suche
Teil 15: Backend meets Frontend: Trainer for kids 5

UI-Elemente leichtgewichtig entwickeln

Sven Ruppert

© Shutterstock / HelenField

Die Rollen und Rechte unserer Webanwendung stehen. Jetzt können wir uns daran machen, die ersten UI-Komponenten zu bauen und zu integrieren. Das heißt natürlich auch, dass wir diese Komponenten testen.

Nachdem wir uns das letzte Mal damit beschäftigt haben, wie wir die Verbindung zwischen den Rollen und Rechten von Apache Shiro mit den Menüpunkten verbinden, werden wir heute ein wenig mehr im Bereich der grafischen Komponenten tätig sein. Ziel ist es, die ersten Matheaufgaben zu stellen, beginnend mit einfachen Additionen und Subtraktionen.

Abb. 1: Die Felder für die Addition

Wir hatten in den vergangenen Teilen einige Komponenten für die jeweiligen Arbeitsbereiche vorgesehen. Eine davon war die Komponente MathComponent. Beginnen wir mit einem trivialen Fall und sehen zwei Ausgabefelder für die Komponenten der Addition sowie ein Eingabefeld für das zu berechnende Ergebnis vor (Abb. 1). Wenn der Benutzer die Aufgabe gelöst und das Ergebnis in das Eingabefeld geschrieben hat, soll nach dem Drücken eines Buttons überprüft werden, ob die Berechnung korrekt gewesen ist. Das vom Computer berechnete Ergebnis soll dann neben dem Ergebnis des Benutzers erscheinen. Zusätzlich gibt es noch einen grafischen Hinweis, ob das Ergebnis korrekt gewesen ist oder nicht. Hierzu verwenden wir einen lächelnden Smiley, wenn es richtig, oder einen weinenden, wenn es falsch gewesen ist.

Das GridLayout aufbauen

Beginnen wir nun mit dem Erstellen der Arbeitsfläche für die Matheaufgaben. Das zugrunde liegende Layout ist diesmal das GridLayout. Hierbei handelt es sich um ein Tabellenkonstrukt, das mit Koordinaten zweidimensional zu adressieren ist. Praktisch bedeutet dies, dass jede Zelle mit einer X- und einer Y-Koordinate versehen ist. Die vollständige Größe wird bei der Erzeugung der Instanz der Klasse GridLayout festgelegt. Sehen wir uns dazu ein Beispiel an. Für das Erzeugen der im GridLayout verwendeten Demo-Buttons definieren wir uns eine Hilfsmethode.

  private Button btn(String caption){
    Button button = new Button(caption);
    button.setSizeFull();
    return button;
  }

Nun befüllen wir das GridLayout der Größe 4×4 mit gleich großen Buttons (Abb. 2).

    final GridLayout grid4X4 = new GridLayout(4, 4);

    grid4X4.addComponent(btn("AA"), 0,0);
    grid4X4.addComponent(btn("BA"), 1,0);
    grid4X4.addComponent(btn("CA"), 2,0);
    grid4X4.addComponent(btn("DA"), 3,0);

    grid4X4.addComponent(btn("AB"), 0,1);
    grid4X4.addComponent(btn("BB"), 1,1);
    grid4X4.addComponent(btn("CB"), 2,1);
    grid4X4.addComponent(btn("DB"), 3,1);

Abb. 1: Das GridLayout mit 4×4 Buttons

Das GridLayout bietet einige Möglichkeiten, um Elemente in übergreifenden Zellen zu positionieren. Als nächstes werden wir einen Button über zwei Zellen in einer Zeile verteilen. Dazu ändern wir den Quelltext wie folgt:

    final GridLayout grid4X4 = new GridLayout(4, 4);

    grid4X4.addComponent(btn("AA"), 0,0);
    grid4X4.addComponent(btn("BA"), 1,0, 2,0);
    grid4X4.addComponent(btn("CA"), 3,0);

    grid4X4.addComponent(btn("AB"), 0,1);
    grid4X4.addComponent(btn("BB"), 1,1);
    grid4X4.addComponent(btn("CB"), 2,1);
    grid4X4.addComponent(btn("DB"), 3,1);

Abb. 3: Der Button BA füllt nicht den gesamten zur Verfügung stehenden Platz aus

Allerdings ist das Ergebnis nicht das erwartete. Der Button BA füllt nicht den gesamten zur Verfügung stehenden Platz aus (Abb. 3). Wenn man ganz genau hinsieht, ist der Button CA an der rechten Seite auch nicht exakt so weit wie der Rand des Buttons DB. Um dieses Verhalten anzupassen, setzen wir das GridLayout auf setSizeFull().

    final GridLayout grid4X4 = new GridLayout(4, 4);

    grid4X4.setSizeFull();
    grid4X4.addComponent(btn("AA"), 0,0);
    grid4X4.addComponent(btn("BA"), 1,0, 2,0);
    grid4X4.addComponent(btn("CA"), 3,0);

    grid4X4.addComponent(btn("AB"), 0,1);
    grid4X4.addComponent(btn("BB"), 1,1);
    grid4X4.addComponent(btn("CB"), 2,1);
    grid4X4.addComponent(btn("DB"), 3,1);

Abb. 4: Das GridLayout nutzt nun den gesamten zur Verfügung stehenden Platz aus

Das GridLayout verwendet nun den gesamten zur Verfügung stehenden Platz im Browser (Abb. 4). Was horizontal funktioniert, ist auch vertikal möglich. Hier anhand des Buttons CA gezeigt (Abb. 5).

    final GridLayout grid4X4 = new GridLayout(4, 4);

    grid4X4.setSizeFull();
    grid4X4.addComponent(btn("AA"), 0,0);
    grid4X4.addComponent(btn("BA"), 1,0, 2,0);
    grid4X4.addComponent(btn("CA"), 3,0, 3,1);

    grid4X4.addComponent(btn("AB"), 0,1);
    grid4X4.addComponent(btn("BB"), 1,1);
    grid4X4.addComponent(btn("CB"), 2,1);

Abb. 5: Auch vertikal Zeilen zusammenzunehmen, ist möglich

Und der Vollständigkeit halber nun noch ein Beispiel, das über Zeilen und Spalten geht (Abb. 6).

    final GridLayout grid4X4 = new GridLayout(4, 4);

    grid4X4.setSizeFull();
    grid4X4.addComponent(btn("AA"), 0,0);
    grid4X4.addComponent(btn("BA"), 1,0, 2,1);
    grid4X4.addComponent(btn("CA"), 3,0);

    grid4X4.addComponent(btn("AB"), 0,1);
    grid4X4.addComponent(btn("CB"), 3,1);

Abb. 6: Beispiel über Zeilen und Spalten hinweg

Komponententests sind ein Klacks

Bei allen Beispielen stellt sich die Frage, wie man mit kurzen Entwicklungszyklen iterativ an der UI arbeiten kann? Bei größeren Projekten kommen vor allem die Startzeiten zum Tragen, die sich ergeben, wenn ein größeres UI initialisiert werden muss. Wir wollen aber lediglich eine Komponente entwickeln, losgelöst von der restlichen Anwendung. Recht praktisch wäre es, wenn man ausschließlich die Komponenten in einem Servlet-Container starten könnte, um dann das Ergebnis in einem Browser zu validieren.

Hierfür ist in diesem Projekt schon alles vorhanden. Denn wir haben die Klasse Main, in der wir eine Instanz des Undertow starten und dabei ein Servlet angeben. Als erstes erzeugen wir uns für die Komponententests eine neue Klasse mit einer main-Methode. In unserem Beispiel ist der Name CalcComponentDevelop. Hier verzichten wir vollständig auf die Integration von Shiro. Sicherheit ist bei der Entwicklung von Komponenten ein nachgelagertes Problem. Ziel ist es, möglichst schnell mit der Initialisierung fertig zu sein. Als nächstes benötigen wir ein Servlet und eine der UI-Klassen.

@WebServlet(value = "/*")
@VaadinServletConfiguration(productionMode = false, ui = ComponentTestUI.class)
public class ComponentDevelopmentTestServlet extends VaadinServlet {}

@Theme("valo")
//  @PreserveOnRefresh
public class ComponentTestUI extends UI {
  private static final Logger LOGGER = LoggerFactory.getLogger(ComponentTestUI.class);
  @Override
  protected void init(VaadinRequest request) {
    setContent(new CalcComponent());
    setSizeFull();
  }
}

In der Main-Methode der Klasse CalcComponentDevelop wird nun genau dieses Servlet gestartet.


//SNIPP
DeploymentInfo servletBuilder
        = Servlets.deployment()
                  .setClassLoader(CalcComponentDevelop.class.getClassLoader())
                  .setContextPath(CONTEXT_PATH)
                  .setDeploymentName("ROOT.war")
                  .setDefaultEncoding("UTF-8")
                  .addServlets(
                      servlet(
                          ComponentDevelopmentTestServlet.class.getSimpleName(),
                          ComponentDevelopmentTestServlet.class).addMapping("/*")
                  );
//SNIPP

Nun haben wir eine sehr kurze Entwicklungs-Test-Schleife (Abb. 7).

Abb. 7: Wir haben nun eine kurze Entwicklungs-Test-Schleife

Backend meets Frontend

In der Artikelserie Backend meets Frontend stellt Sven Ruppert (Vaadin) Konzepte und Technologien rund um das UI-Framework Vaadin vor. Sein Fokus liegt dabei auf modernem Web-Design für Java-Backend-Entwickler.

Zum ersten Teil und damit dem Start der Tutorien rund um die UI-Entwicklung mit Java geht es hier entlang. Alle Teile der Serie Backend meets Frontend finden sich hier.

Implementierung des Menüpunkts

Für die Entwicklung haben wir nun unsere Werkzeuge zusammen. Beginnen wir also damit unser UI für den Menüpunkt zu implementieren. Hier gehen wir recht geradlinig vor, da wir die Anzahl der Elemente und deren Position schon haben. Zu Beginn definieren und erzeugen wir alle gewünschten Elemente.

  public static final int COLUMNS = 7;
  public static final int ROWS = 2;

  private GridLayout grid;

  private TextField valueOne;
  private TextField valueTwo;
  private TextField humanResult;
  private TextField machineResult;
  private Label resultLabel;

  private Button buttonNext;
  private Button buttonCalculate;

  private Random random;

  private void initFields() {
    random = new Random(System.nanoTime());

    valueOne = new TextField();
    valueTwo = new TextField();
    humanResult = new TextField();
    machineResult = new TextField();
    resultLabel = new Label();

    buttonNext = btn("Next");
    buttonCalculate = btn("OK");

    grid = new GridLayout(COLUMNS, ROWS);
  }

Da nun alle Elemente vorhanden sind, bauen wir die Grundstruktur auf.

  private void createStructure() {
    grid.addComponent(valueOne, 0, 0);
    grid.addComponent(new Label(PLUS.getHtml(), HTML), 1, 0);
    grid.addComponent(valueTwo, 2, 0);
    grid.addComponent(new Label("="), 3, 0);
    grid.addComponent(humanResult, 4, 0);
    grid.addComponent(buttonCalculate, 5, 0);
    grid.addComponent(buttonNext, 6, 0);

    grid.addComponent(machineResult, 4, 1);
    grid.addComponent(resultLabel, 5, 1);
  }

Nun können wir schon die finale Struktur sehen und testen, ob es dem entspricht, was wir erreichen wollen. Dann beginnen wir mit dem Anpassen der Elemente selbst. Zum Beispiel, dass der Button Next zu Beginn deaktiviert ist, die Texteingabefelder leer sind und das Textfeld für die berechneten Werte auf read-only gesetzt wird. Ebenfalls sorgen wir für eine initiale Aufgabe, indem wir die Textfelder für die Werte A und B mit jeweils einer Zufallszahl befüllen.

  private void customizeFields() {
    machineResult.setReadOnly(true);
    machineResult.setReadOnly(true);
    buttonNext.setEnabled(false);

    //clear
    valueOne.setValue("");
    valueTwo.setValue("");

    humanResult.setValue("");
    machineResult.setValue("");

    //toggle
    buttonCalculate.setEnabled(true);
    buttonNext.setEnabled(false);

    //init next
    valueOne.setValue(String.valueOf(random.nextInt(10)));
    valueTwo.setValue(String.valueOf(random.nextInt(10)));
  }

Jetzt fehlen nur noch die Implementierungen der beiden Buttons, um die Funktion abzubilden. Wir nehmen uns als erstes den einfacheren Fall vor, das Zurücksetzen der Komponente mit dem Next Button.

    buttonNext.addClickListener((Button.ClickListener) event -> {
      //clear
      valueOne.setValue("");
      valueTwo.setValue("");

      humanResult.setValue("");
      machineResult.setValue("");

      resultLabel.setCaption("");

      //toggle
      buttonCalculate.setEnabled(true);
      buttonNext.setEnabled(false);

      //init next
      valueOne.setValue(String.valueOf(random.nextInt(10)));
      valueTwo.setValue(String.valueOf(random.nextInt(10)));
    });

Und zu guter Letzt der Button OK, um die Berechnung selbst zu validieren.

    buttonCalculate.addClickListener((Button.ClickListener) event -> {
      String valueOneValue = valueOne.getValue();
      String valueTwoValue = valueTwo.getValue();

      Integer a = Integer.valueOf(valueOneValue);
      Integer b = Integer.valueOf(valueTwoValue);

      Integer x = a + b;

      machineResult.setValue(String.valueOf(x));

      //Compare Result
      Integer wasRight = Integer.valueOf(humanResult.getValue()) - x;
      if (wasRight == 0) {
        resultLabel.setCaption(CHECK_CIRCLE.getHtml());
        resultLabel.setCaptionAsHtml(true);

      } else {
        resultLabel.setCaption(CLOSE_CIRCLE.getHtml());
        resultLabel.setCaptionAsHtml(true);
      }
      resultLabel.setCaptionAsHtml(true);
      buttonCalculate.setEnabled(false);
      buttonNext.setEnabled(true);
    });

An dieser Stelle sehen wir schon, dass wir einige Dinge miteinander vermischen. Das Extrahieren der Logik, Validieren der Eingabefelder und ähnliches werden wir uns in den nächsten Teilen dann genauer ansehen. Wir haben nun alles zusammen und können die Komponente einzeln testen (Abb. 8).

Abb. 8: Der Workflow steht

Zu guter letzt muss natürlich die Komponente noch in die Anwendung selbst integriert werden. Hierzu müssen wir an der Stelle lediglich die Klasse Main starten und in der Anwendung bis zum Mathe-Dashboard navigieren (Abb. 9). Weitere Anpassungen sollten nicht notwendig sein, da ja dieselbe Komponente initialisiert wird.

Abb. 9: Alle Komponenten sind in die Anwendung integriert

Fazit

In diesem Teil haben wir uns damit beschäftigt, wie wir eine einzelne Komponente entwickeln können und gesehen, dass es sinnvoll sein kann, wenn man diese Komponentenentwicklung in eine leichtgewichtige Umgebung auslagert. In unserem Fall bedeutet dies, dass wir die Komponente einzeln in einem Servlet-Container gestartet haben, ohne dass die gesamte Anwendung jedesmal initialisiert werden muss. So sind Zyklen von wenigen Sekunden möglich, obwohl der Servlet-Container jedesmal komplett neu gestartet wird. Damit können wir auch annehmen, das Fehler vom vorherigen Durchlauf so gut wie nie Einfluss auf den aktuellen Durchlauf haben. Diesmal haben wir das noch explizit für diese eine Komponente gemacht. In den nächsten Teilen werden wir uns ansehen, wie man das allgemeiner für die Entwicklung einsetzen kann.

Den Quelltext findet ihr auf GitHub. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.