Suche
Teil 19: Backend meets Frontend: Trainer for kids 9

Die ersten grafischen Belohnungen

Sven Ruppert

© Shutterstock / HelenField

Unsere Anwendung soll rechnen lernen und die Ergebnisse grafisch darstellen, dafür setzen wir die CalcComponent ein. Bei der Implementierung der vier Grundrechenarten gibt es allerdings einige Dinge zu beachten, gerade im Hinblick auf die Division.

In diesem Teil werden wir uns jetzt um die CalcComponent kümmern. Bisher hatten wir eine einfache Aufgabe gestellt und das Ergebnis das der Benutzer angibt nach dem Drücken des Ok Buttons mit dem vom Computer errechneten Ergebnis verglichen. Als kurze Erinnerung, so sieht es aus:

Als nächstes kommt nun die Variation der Grundrechenarten.

Die Grundrechenarten

Zusätzlich zu den Quelltextbeispielen zu diesem Artikel verwende ich auch die Sourcen des Open-Source -Projekts Functional-Reactive. Die Sourcen befinden sich auf GitHub. Ebenfalls werde ich damit beginnen, funktionale Aspekte in die Entwicklung einfließen zu lassen. Hierzu stütze ich mich auf die Serie hier auf JAXenter unter dem Namen Checkpoint Java.

Wenn man sich nun die Grundrechenarten ansieht, dann kann man erkennen, dass die Addition, Multiplikation und die Subtraktion sehr gleichförmig sind. Einzige Ausnahme ist dabei die Division.

Beginnen wir mit den drei zuerst genannten Grundrechenarten: Hier können wir einfach nur das Zeichen zwischen den beiden ersten Texteingabenfeldern austauschen. Es stellt sich nun die Frage, wie man das aussuchen bzw. dem Benutzer anbieten sollte.

Als erstes wird vereinfachend angenommen, dass es keine Selektion der Grundrechenart durch den Benutzer geben wird. Somit entscheidet die Anwendung, welche Variante als nächstes kommen wird. Da es sich hier um einen Trainer für Kinder handelt, sollte noch besonderes Augenmerk darauf gelegt werden, die gerade selektierte Grundrechenart deutlich erkennbar und sichtbar zu gestallten. (Was hier definitiv noch nicht der Fall ist.)

BasicArithmetics – Operator

Gehen wir davon aus, dass wir zu Beginn immer nur zwei Eingangswerte haben, die auf einen Ergebniswert gemappt werden, so können wir für die zu betrachtenden vier Grundrechenarten vier Funktionen definieren.

public static BiFunction<Integer, Integer, Float> add() {
    return (a , b) -> (float) (a + b);
  }

  public static BiFunction<Integer, Integer, Float> sub() {
    return (a , b) -> (float) (a - b);
  }

  public static BiFunction<Integer, Integer, Float> mul() {
    return (a , b) -> (float) (a * b);
  }

  public static BiFunction<Integer, Integer, Float> div() {
    return (a , b) -> (float) (a / b);
  }

Allerdingas ist das doch recht viel Quelltext für sehr wenig Inhalt. Da die Anzahl der Grundrechenarten begrenzt ist, können wir hier mit einer Enumeration arbeiten. Alle Elemente, die zu einem Operator zugeordnet werden, sind direkt in der Enumeration enthalten. Hierzu zählen in diesem Fall das logische Zeichen ( + – * : ) und die zu verwendenden Funktionen.

public enum Operator {

    ADD((a , b) -> (float) (a + b), "+"),
    SUB((a , b) -> (float) (a - b), "-"),
    MUL((a , b) -> (float) (a * b), "*"),
    DIV((a , b) -> (float) (a / b), ":");

    private String sign;
    private BiFunction<Integer, Integer, Float> fkt;

    Operator(BiFunction<Integer, Integer, Float> fkt, String sign) {
      this.fkt = fkt;
      this.sign = sign;
    }

    public BiFunction<Integer, Integer, Float> fkt() {
      return fkt;
    }

    public String sign(){return sign;}
}

Nun soll die Anwendung noch auswählen, welche Grundrechenart als nächstes verwendet werden wird. Die Auswahl, welche der Grundrechenarten kommen wird, können wir einfach mit der Klasse Random abbilden und uns Zufallswerte zum Beispiel von 0 bis 3 liefern lassen, die wir dann einer Grundrechenart zuordnen.

private static final Random random = new Random(System.nanoTime());

    public static Operator rndOperator(){
      final Operator[] values = Operator.values();
      return values[random.nextInt(values.length)];
    }

Nun haben wir noch die Besonderheit, dass wir in dem vorhin gezeigten grafischen Darstellung lediglich die drei Grundrechenarten – Addition, Subtraktion und Multiplikation – abbilden möchten. Das hat den einfachen Grund, dass es dabei nur ganzzahlige Ergebnisse gibt. Für die Division werden wir einen etwas anderen Weg gehen.

Somit benötigen wir eine Methode, die uns lediglich einen zufälligen Operator liefert, ohne dabei die Division zu verwenden.

 public static Operator rndNoDivOperator(){
      final Operator[] values = Operator.values();
      return values[random.nextInt(values.length-1)];
    }

Nun haben wir alles zusammen und erhalten für unsere Enumeration die nachfolgende Implementierung.

public enum Operator {

    ADD((a , b) -> (float) (a + b), "+"),
    SUB((a , b) -> (float) (a - b), "-"),
    MUL((a , b) -> (float) (a * b), "*"),
    DIV((a , b) -> (float) (a / b), ":");

    private String sign;
    private static final Random random = new Random(System.nanoTime());
    private BiFunction<Integer, Integer, Float> fkt;

    Operator(BiFunction<Integer, Integer, Float> fkt, String sign) {
      this.fkt = fkt;
      this.sign = sign;
    }

    public BiFunction<Integer, Integer, Float> fkt() {
      return fkt;
    }

    public String sign(){return sign;}

    public static Operator rndOperator(){
      final Operator[] values = Operator.values();
      return values[random.nextInt(values.length)];
    }

    public static Operator rndNoDivOperator(){
      final Operator[] values = Operator.values();
      return values[random.nextInt(values.length-1)];
    }
  }

BasicArithmetics – NextTask

Wir sind nun im Besitz des Operators, und es fehlt noch die Darstellung einer Aufgabe. Für eine Aufgabe benötigen wir zwei Eingangswerte und den Operator. Das Ergebnis ist nur eine Abbildung der Funktion, die schon im Oprator enthalten ist.

Hier bediehnen wir uns der Klasse aus der Funktional-Reactive-Library (Link siehe weiter oben) mit dem Namen Tripel. Wer ein wenig mehr darüber wissen möchte, und eine etwas andere Lösung zu diesem Problem lesen will, dem empfehle ich Checkpoint Java: Funktional mit Java 9.

public static class BasicArithmeticTask
      extends Tripel<Integer, Integer, Operator> {

    public BasicArithmeticTask(Integer a , Integer b , Operator o) {
      super(a , b , o);
    }

    public Integer a() {return getT1();}

    public Integer b() {return getT2();}

    public Operator operator() {return getT3();}
  }

Um dem Entwickler nun eine einfache und komfortable Möglichkeit zu geben, eine weitere Aufgabe zu erhalten, werden drei Methoden definiert.

Als erstes die allgemeine Methode zum Erzeugen einer Aufgabe. Hier wird der Operator als auch die Grenze für die Größe eines Eingabewertes angegeben.

 public static BiFunction<Operator, Integer, BasicArithmeticTask> nextTask() {
    return (op , bound) -> {
      final Random random = new Random(nanoTime());
      return new BasicArithmeticTask(
          random.nextInt(bound) ,
          random.nextInt(bound) ,
          op
      );
    };
  }

Da wir wissen, dass dies mit der zufälligen Auswahl eines Operators kombiniert werden soll, werden zwei weitere Methoden definiert. Die erste liefert eine Zufallsaufgabe, bei der auch eine Division vorkommen kann, die zweite funktioniert ohne Division.

public static Function<Integer, BasicArithmeticTask> nextRndTask() {
    return (bound) -> nextTask().apply(Operator.rndOperator(), bound);
  }

  public static Function<Integer, BasicArithmeticTask> nextRndNoDivTask() {
    return (bound) -> nextTask().apply(Operator.rndNoDivOperator(), bound);
  }

Die Verwendung in der grafischen Komponente ist recht einfach, es muss an der Stelle lediglich der Wert in die jeweilige Zielkomponente geschrieben werden.

 task = nextRndNoDivTask().apply(10);
      operator.setValue(task.operator().sign());
      valueOne.setValue(valueOf(task.a()));
      valueTwo.setValue(valueOf(task.b()));

Soweit ist nun alles funktionsfähig. Die Anwendung ist allerdings noch sehr anfällig bei falschen Eingaben durch den Benutzer. Es werden also im Moment noch eine Menge NumberFormatExceptions zum Vorschein kommen. Das Problem werden wir in einem der nächsten Teilen behandeln. Jetzt kommt erst einmal als nächstes die Division.

„BasicArithmetics“ – Division

Bei der Division ist das neue zu betrachtende Element die nicht mehr ganzzahligen Ergebnisse. An dieser Stelle wird erst einmal eine einfache Realisierung gezeigt. Wenn man nur ganzzahlige Ergebnisse zulassen möchte, so kann man die Aufgabe aus einer Multiplikation ableiten. 5*3=15 -> 15:3=5 oder 15:5=3 Das Prinzip ist sehr einfach, da wir auch die gleichen Grenzen für die größte zu generierende Zahl nehmen können. In diesem Fall ist es die Zahl zehn, womit wir in der Multiplikation ein Ergebnis von maximal 100 erhalten können. Wenn das auch zu dem Rahmen passt, in dem die Division stattfinden darf, sind wir an dieser Stelle fertig.

Nun ist das noch in die Erzeugung der Aufgaben mit einzubringen. Hierzu wird die Stelle, bzw. die Funktion mit der die nächste Aufgabe erzeugt wird, ein wenig erweitert. An der Stelle möchte ich anmerken, dass die Kopplung der Besonderheiten sehr hart ist, jedoch für unser Beispiel noch ausreichend in der Realisierung ist. Das wird später aufgehoben, sobald mehr Anforderungen an die zu generierende Aufgabe definiert werden.

Als erstes werden die beiden Eingagswerte generiert. In der nachfolgenden switch-Anweisung werden dann die einzelnen Fälle betrachtet. Für die Addition und Multiplikation ist keine Besonderheit vorgesehen, diese werden durch den default Zweig abgedeckt. Für die Division wird einfach als erster Wert die Multiplikation der beiden generierten Werte genommen. Der zweite Wert ist weiterhin der zweite generierte Wert.

Hier wurde noch eine Veränderung vorgenommen. Bisher ist es möglich gewesen, dass bei der Subtraktion ein Wert kleiner null als Ergebnis vorkommen kann. Das kann immer dann passieren, wenn der zweite Wert, der generiert wird, größer als der erste Wert ist. In diesem Fall werden dann einfach die beiden Werte vertauscht, sodass immer Ergebnisse herauskommen werden, die größer als null sind.

public static BiFunction<Operator, Integer, BasicArithmeticTask> nextTask() {
    return (op , bound) -> {
      final Random random = new Random(nanoTime());
      final int a = random.nextInt(bound);
      final int b = random.nextInt(bound);

      switch (op) {
//        case ADD:break;
//        case MUL:break;
        case SUB:
          return (a > b) ? new BasicArithmeticTask(a , b , op)
                         : new BasicArithmeticTask(b , a , op);
        case DIV: return new BasicArithmeticTask((a*b) , b , op);
        default:
          return new BasicArithmeticTask(a , b , op);
      }
    };
  }

Nun sind alle vier Grundrechenarten abgebildet und die Übungen können beginnen. Wenn man das nun ausprobiert, so wird die Motivation jüngerer Benutzer nicht übermäßig sein. Es fehlt also noch eine Darstellung der bisherigen Leistungen.

Die erste Belohnung

Sicherlich gibt es viele Möglichkeiten, eine Belohnung zu vergeben. An dieser Stelle wird einfach eine kleine visuelle Darstellung gewählt, mit der deutlich zu sehen ist, ob mehr Aufgaben erfolgreich bearbeitet worden sind. Vereinfachend werden alle Grundrechenarten gleichwertig als eine Aufgabe gewertet. Also unabhängig von der Art der Aufgabe wird einfach nur dargestellt, ob es ein richtiges Ergebnis gewesen ist oder nicht.

Charts

Um Daten kompakt und ansehnlich darzustellen, eignen sich Charts sehr gut. Die Daten, die hier dazustellen sind, sind noch sehr überschaubar. Handelt es sich doch nur um zwei Werte, nämlich die Anzahl der erfolgreich und nicht erfolgreich absolvierten Aufgaben.

Mit jedem Durchlauf werden die Zahlen aktualisiert und sind nur gültig, solange die Ansicht nicht gewechselt wird. Somit werden die Daten auch in der Komponente transient gehalten. Die Darstellung soll hier als erstes in Form eines Balkendiagramms erfolgen. Ein Balken für die fehlerhaften Ergebnisse und ein Balken für die Anzahl der erfolgreich beantworteten Aufgaben.

Um die Charts zu verwenden, kann man auf die kommerziellen Add-ons zurückgreifen. Wer sich das alles einfach einmal ansehen und ausprobieren möchte, kann sich dort eine voll funktionsfähige, zeitlich beschränkte Testversion herunterladen.

Als erstes definieren wir ein Attribut vom Typ Pair<Integer,Integer> mit dem Namen result, in dem wir die jeweils aktuelle Kombination von positiver und negativer Anzahl gelöster Aufgaben halten werden.

Der richtig Zeitpunkt, zu dem wir das Ergebnis aktualisieren können, ist immer dann gegeben, wenn der Button OK gedrückt worden ist, um zu prüfen, ob das Ergebnis richtig ist. Dann liegen alle notwendingen Informationen vor.

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

      Float x = task.operator().fkt().apply(a , b);

      machineResult.setValue(valueOf(x));

      Float wasRight = Float.valueOf(humanResult.getValue()) - x;
      if (wasRight == 0) {
        resultLabel.setCaption(CHECK_CIRCLE.getHtml());
        resultLabel.setCaptionAsHtml(true);
        results = Pair.next(results.getT1()+1, results.getT2());
      } else {
        resultLabel.setCaption(CLOSE_CIRCLE.getHtml());
        resultLabel.setCaptionAsHtml(true);
        results = Pair.next(results.getT1(), results.getT2()+1);
      }

Die Werte sind nun vorhanden, müssen also jetzt nur noch grafisch dargestellt werden. Der Platz für die grafische Darstellung ist erst einmal unter der Aufgabe. Hierzu erweitern wir das GridLayout um eine weitere Zeile, indem wir die Konstante ROWS in der Komponente von 2 auf 3 erhöhen.

Diese Zeile wiederum wird vollständig von einem Element in Anspruch genommen. Den provisorischen Platzhalter hierfür erzeugen wir mit einem Label und setzen es an die dafür vorgesehene Stelle. In der Methode createStructure fügen wir nun eine weitere Zeile zur Definition des Grid hinzu: grid.addComponent(new Label("RESULT - CHART"), 0, 2, 6,2);

 private void createStructure() {
    grid.addComponent(valueOne , 0 , 0);
    grid.addComponent(operator , 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);

    grid.addComponent(new Label("RESULT - CHART"), 0, 2, 6,2);
  }

Nachdem der Test erfolgreich durchlaufen worden und die Anzeige des Labels an der richtigen Stelle zu sehen gewesen ist, kann nun die Erzeugung der Instanz dieses Labels in eine Methode extrahiert werden. In dieser Methode wird nachfolgend die Instanz vom Typ Chart erzeugt werden.

createChart

Um nun mit dem Add-on „Charts“ zu arbeiten, muss man als erstes die Abhängigkeit in der pom.xml defineiren. Hierzu fügen wir die folgenden Zeilen hinzu. Die Versionsnummer ist natürlich der aktuellen anzupassen.

<!--Vaadin ProTools-->
      <dependency>
        <groupId>com.vaadin</groupId>
        <artifactId>vaadin-charts</artifactId>
        <version>4.0.0</version>
      </dependency>

Ziel ist es, ein Balkendiagramm zur Darstellung der Ergebnisse zu erzeugen. Hierzu kann man sich die Dokumentation auf der Vaadin-Webseite ansehen. Die Durchsicht der Liste aller verfügbaren Charttypen ergibt, dass wir ein Diagramm vom Typ Bar benötigen. Diese definieren wir einfach als Attribut in unserer Komponente und verwenden diese Instanz anstelle des vorher verwendeten Labels. private final Chart chart = new Chart(ChartType.BAR); Wenn nun die Anwendung das erste Mal mit diesem Diagramm gestartet wird und der Benutzer die CalcComponent zur Anzeige bringt, bekommt man den folgenden Fehler:

Bisher wurde ausschließlich das DefaultWidgetSet verwendet. Durch die Verwendung der Charts reicht dieses allerdings nicht mehr aus. Es muss jetzt eine Kombination von DefaultWidgetSet und dem WidgetSet von den Charts erzeugt werden.

WidgetSet

Um das zu verwendende WidgetSet zu definieren wird die Annotation @Widgetset eingesetzt und vom Vaadin Framework ausgewertet. Beim Start der Anwendung, bzw. der Session, wird ein Event vom Typ UICreateEvent erzeugt. Die Klasse, die dort als UI-Klasse des Events geliefert wird, wird dann auch nachfolgend ausgewertet, um an die Annotation zu gelangen.

 public WidgetsetInfo getWidgetsetInfo(UICreateEvent event) {
        Widgetset uiWidgetset = getAnnotationFor(event.getUIClass(),
                Widgetset.class);

        // First case: We have an @Widgetset annotation, use that
        if (uiWidgetset != null) {
            return new WidgetsetInfoImpl(uiWidgetset.value());
        }

        // Second case: We might have an init parameter, use that
        String initParameterWidgetSet = event.getService()
                .getDeploymentConfiguration().getWidgetset(null);
        if (initParameterWidgetSet != null) {
            return new WidgetsetInfoImpl(initParameterWidgetSet);
        }

        // Find the class AppWidgetset in the default package if one exists
        WidgetsetInfo info = getWidgetsetClassInfo();

        // Third case: we have a generated class called APP_WIDGETSET_NAME
        if (info != null) {
            return info;
        }

        // Fourth case: we have an AppWidgetset.gwt.xml file
        else {
            InputStream resource = event.getUIClass()
                    .getResourceAsStream("/" + APP_WIDGETSET_NAME + ".gwt.xml");
            if (resource != null) {
                return new WidgetsetInfoImpl(false, null, APP_WIDGETSET_NAME);
            }
        }

        // fifth case: we are using the default widgetset
        return null;
    }

Die Annotation @WidgetSet selbst verfügt nur über ein Attribut vom Typ String. Dort wird das zu verwendende WigetSet angegeben. Der Name selbst besteht aus einer Packageangabe und dem Namen der Datei selbst, eine Dateiendung gwt.xml ist nicht Teil des Namens.

Die Datei, in unserem Fall mit dem Namen VaadinJumpstartWidgetset.gwt.xml, beinhaltet alle Namen der zu inkludierenden WidgetSets. Es können auch weitere Dinge definiert werden, was allerdings in unserem Beispiel gerade nicht nötig ist.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.5.1//EN"
        "http://google-web-toolkit.googlecode.com/svn/tags/2.5.1/distro-source/core/src/gwt-module.dtd">
<module>
    <inherits name="com.vaadin.DefaultWidgetSet"/>
    <inherits name="com.vaadin.addon.charts.Widgetset" />
</module>

Nun haben wir das WidgetSet vom ProTool Charts hinzugefügt. Der nächste Schritt besteht nun darin, dass WidgetSet neu zu kompilieren. Am einfachsten ist das mit dem Vaadin-Maven-Plug-in zu erledigen. Einfach das Target vaadin::update-widgetset ausführen.

Entkopplung von der Klasse UI

Im letzten Teil wurde die UI-Klasse in der Art des Basismoduls getrennt, das in dem Servlet einfach die Klasse UI angegeben worden ist und dann der UIProvider selbst implementiert wurde. Dort wurde dann mittels DI-Framework eine passende Implementierung zu der abstrakten Klasse gesucht und diese stattdessen verwendet, um eine Instanz zu erzeugen.

Nun stellt sich heraus, das dies nun ledier nicht mehr funktioniert, denn es wird nur die ursprünglich angegebene Klasse ausgewertet. Und hier sind wir also wieder bei den Framework-internen Dingen.

Die Lösungn ist einfacher als vermutet: Bisher sind wir davon ausgegangen, dass wir überhaupt nur eine UI-Klasse in unserer Anwendung benötigen. Da wir aber über ein DI-Framework verfügen, können wir das ebenfalls loswerden.

Im generischen Vaadin-Modul erweitern wir unsere UI-Klasse JumpstartUI indem wir die Annotationen hinzufügen, die wir benötigen. Unter anderem die des WidgetSet und zum Beispiel @PreserveOnRefresh. Doch in der UI-Klasse, dessen Instanz ja schon von dem DI-Container verwaltet wird, wird eine Instanz einer Komponenten-Factory injiziert. Da wir nun der Abstraktion noch einen Level hinzufügen, werden wir auch gleich einen SessionService und einen SecurityService injizieren. Diese haben lediglich Signaturen, deren Klassen auch im Modul zur Verfügung stehen. In unserem Fall entfernen wir die Notwendigkeit, das Interface User zu kennen. Die Methode wird so umformuliert, dass sie mit einem Boolean auf die Frage antworten kann, ob der Benutzer in dieser Session gerade angemeldet ist. Wie das genau herausgefunden wird, ist dann wieder ein Implementierungsdetail der finalen Implementierung.

@PreserveOnRefresh
@Widgetset("org.rapidpm.vaadin.server.VaadinJumpstartWidgetset")
public class JumpstartUI extends UI {

  private static final Logger LOGGER = LoggerFactory.getLogger(JumpstartUI.class);


  @Inject private SessionService userService;
  @Inject private SecurityService securityService;
  @Inject private JumpstartUIComponentFactory jumpstartUIComponentFactory;

  @Override
  protected void init(VaadinRequest vaadinRequest) {
    LOGGER.debug("init - request = " + vaadinRequest);
    if (! (isUserPresent() && isRemembered()))
      setContent(jumpstartUIComponentFactory.createComponentToSetAsContent(vaadinRequest));
    setSizeFull();
  }

  private boolean isRemembered() {
    return securityService.isRemembered();
  }

  private boolean isUserPresent() {
    return userService.isUserPresent();
  }
}

Die Interfaces, deren Implementierung hier injiziert werden, sind zum Beispiel in einem allgemeinen API-Modul definiert.

@FunctionalInterface
public interface JumpstartUIComponentFactory extends Serializable {
  Component createComponentToSetAsContent(VaadinRequest vaadinRequest);
}

public interface SecurityService {
  boolean isRemembered();
}

public interface SessionService {
  boolean isUserPresent();
}

In dem Modul der Anwendung selbst befindet sich dann zum Beispiel die Implementierung der JumpstartUIComponentFactory.

public class MyUIComponentFactory implements JumpstartUIComponentFactory {

  @Inject LoginComponent loginScreen;

  @Override
  public Component createComponentToSetAsContent(final VaadinRequest vaadinRequest) {
    return loginScreen;
  }
}

Hieraus ergeben sich weitere Dinge. Werden mehrere Anwendungen mit dem selben WidgetSet produziert, so kann man dies nun an einer zentralen Stelle realisieren. Werden aber in jeder Anwendung Variationen davon benötigt, so befindet sich die UI-Klasse inklusive dem Servlet wieder in der Anwendung.

Zu empfehlen ist es allerdings immer, diese Dinge in ein separates Modul zu verlegen, damit der Erstellungsprozess des WidgetSet nur, wenn es wirklich nötig ist, durchlaufen wird. Das erneute Erzeugen eines WidgetSet kann unter Umständen einige Minuten dauern.

Zurück zu den Charts

Da nun die Grundlagen gelegt worden sind, um Charts anzuzeigen, beginnt die Arbeit an der Darstellung selbst. Hier wird eine sehr einfache Version verwendet.

Prinzipiell wird eine Instanz von Typ Chart immer auf dem selben Wege erzeugt. Zuerst wird unter Angabe des gewünschten Chart-Type die Instanz selbst erzeugt. private final Chart chart = new Chart(ChartType.BAR);

Nachfolgend werden dann die gewünschten Attribute in der dazugehörigen Konfiguration modifiziert. Es gibt eine Methode zum setzen der Konfiguration, jedoch ist es von den Entwicklern der Charts eher gewollt, dass die Instanz der Konfiguration von der gerade erzeugten Instanz geholt wird.

final Configuration configuration = chart.getConfiguration();

Die Instanz der Konfiguration wird nun modifiziert. Dieses Vorgehen sollte bei allen Elementen der Konfiguration angewendet werden. Meiner Meinung ist das nicht intuitiv, jedoch scheint es hier von den Entwicklern die favorisierte Vorgehensweise zu sein.

In diesem Beispiel werden lediglich die X-Achse und Y-Achse konfiguriert, die Legende wird deaktiviert. Nicht zu vergessen: Natürlich wird die ID der Komponente ebenfalls gesetzt, damit auch diese in den Tests einfach und direkt zu adressieren ist.

chart.setId(CHART_RESULT_ID);
    final Configuration configuration = chart.getConfiguration();

    configuration.setTitle(property(CALC_COMPONENT_CHART_RESULT_TITLE));
    configuration.getLegend().setEnabled(false);

    final XAxis xAxis = configuration.getxAxis();
    xAxis.setCategories(
        property("calc.component.chart.result.good") ,
        property("calc.component.chart.result.bad")
    );

    final YAxis yAxis = configuration.getyAxis();
    yAxis.setTitle(property("calc.component.chart.result.yAxis"));

Was sind Charts ohne Daten? Natürlich recht wenig aussagekräftig. Die Daten die zur Anzeige gebracht werden sollen, werden aktiv gesetzt. Hierzu wird die Methode setData(..) verwendet. Die Daten werden in Form von Datensätzen angegeben. Der vorliegende Fall ist sehr einfach, da es sich immer nur um zwei einzelne Zahlen handelt.

series.setData(results.getT1() , results.getT2());
configuration.setSeries(series);

Immer dann, wenn nun die Daten aktualisiert werden, muss man diese Daten dem Chart übergeben und unter Umständen ein erneutes Zeichnen der Komponente triggern. Das ist in dieser Anwendung immer der Fall, wenn der Button OK gedrückt worden ist. Es wird also der ClickListener erweitert. Die hierfür notwendingen Zeilen sind das Berechnen der neuen Werte, zum Beispiel results = Pair.next(results.getT1() + 1 , results.getT2()); und der Anstoß zum erneuten Zeichnen.

series.setData(results.getT1() , results.getT2());

chart.drawChart();

Hier ein Auszug aus der Implementierung des ClickListener.

Float wasRight = Float.valueOf(humanResult.getValue()) – x;

      if (wasRight == 0) {
        resultLabel.setCaption(CHECK_CIRCLE.getHtml());
        resultLabel.setCaptionAsHtml(true);
        results = Pair.next(results.getT1() + 1 , results.getT2());
      } else {
        resultLabel.setCaption(CLOSE_CIRCLE.getHtml());
        resultLabel.setCaptionAsHtml(true);
        results = Pair.next(results.getT1() , results.getT2() + 1);
      }
      series.setData(results.getT1() , results.getT2());
      chart.drawChart();

Damit ist das Diagramm im Aufbau fertig und kann verwendet werden.

Ablage der Ergebnisse

Im Moment ist das Ergebnis in der Komponente abgelegt. Das führt natürlich dazu, dass diese Ergebniswerte nicht mehr verfügbar sind, wenn man einmal auf eine andere Ansicht wechselt. Dieses Verhalten wird einem sehr schnell bewusst, sobald ein jüngerer Benutzer nach einigen durchgerechneten Aufgaben einmal in der Anwendung ein wenig durch die Bereiche navigiert und bei Rückkehr zu dieser Ansicht seine Ergbnisse nicht mehr vorfindet.

Eine Lösung besteht darin, die Werte in der Session selbst abzulegen. Es ist nur eine Frage der Zeit, bis auch die Frage aufkommt, ob die Daten über einen längeren Zeitraum gehalten werden können. Das Thema persistente Daten wird zu einem späteren Zeitpunkt behandelt.

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.

Fazit

Nun sind die ersten grafischen Belohnungen verfügbar, jedoch sind die Möglichkeiten der Diagramme bei weitem nicht ausgeschöpft. Wenn man nun darstellen möchte, wie die Bruchrechnung funktioniert, so kann man die jeweiligen Brüche als Tortendiagramme darstellen. Als Beispiel möge die folgende Aufgabe herhalten:

1/2 + 1/3 = ?? 

Wie lässt sich dies nun darstellen? Eine Kleinigkeit, die mir von meinem Sohn (6 Jahre) gesagt worden ist. Als er nur sehr wenige falsche Ergebnisse hatte und es eine kleine aber sichtbare Darstellung dieser Fehler gegeben hatte, fragte er nach der Möglichkeit, diese Fehlerdarstellung zu eliminieren. Zum Beispiel durch zusätzlich zu lösende Aufgaben. Es stand also die Frage im Raum, wie man durch eine zu lösende Aufgabe nun kein weiteres Plus bekommt, sondern ein Minus ausgleicht.

Die Diskussion dauerte ein wenig länger, wurde mir doch an dieser Stelle bewusst, das Kinder hier ein sehr ausgeprägtes Gerechtigkeitsempfinden haben. Nur einige Punkte die aufgekommen sind als Anforderung:

  • Eine fehlerhafte Addition ist nur durch eine erfolgreiche Addition zu beheben.
  • Eine Aufgabe zum Eliminieren von Minuspunkten muss schwerer sein.
  • Eine falsch gelöste Extraaufgabe, die schwerer gewesen ist, zählt weder positiv noch negativ, wurde sie doch freiwillig gerechnet.
  • Und vieles mehr…

Ich kann es wirklich empfehlen, solche Diskussion als Vorbereitung für die nächsten Anforderungsanalysemeetings ruhig und ohne zu verzweifeln durchzustehen.

Den Quelltext findet ihr auf GitHub. Screencasts findet ihr unter Youtube - Vaadin Developer

Bei Fragen und/oder 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.