GWT für Anspruchsvolle

Google Web Toolkit – UI-Entwicklung mit dem Activities and Places Framework
Kommentare

Die Anforderungen an komplexe User Interfaces in Webapplikationen werden höher und die Technologien zum Erzeugen derselben immer leistungsfähiger. Neben einer intuitiven Benutzerführung werden das Setzen von Bookmarks oder die Verwendung der Browserbuttons und des Browserverlaufs vorausgesetzt. Zur Bereitstellung dieser Funktionalitäten kann das im Google Web Toolkit (GWT) integrierte „Activities and Places Framework“ verwendet werden. Strukturiertheit und Skalierbarkeit spielen auch bei der Entwicklung des Codes der Webapplikation eine wichtige Rolle und sollen nicht nur Eigenschaften der Webseite für den Endbenutzer sein. Die Versuchung ist oft groß, die darstellende Schicht einer Applikation und die Businesslogik nicht strikt zu trennen. Zur Lösung dieses Problems kann für die Entwicklung von User Interfaces mit GWT das Model-View-Presenter-(MVP-)Software-Pattern eingesetzt werden.

Das Activities and Places Framework erlaubt das Management des Browserverlaufs und die damit verbundene Bedienung der VOR-, ZURÜCK- und REFRESH-Buttons, die jeder gängige Browser zur Verfügung stellt. Seit der Version 2.1. ist dieses Framework in GWT integriert. Das Grundprinzip ist, dass jedem User Interface (UI) eine Activity und ein Place zugeordnet ist.
Die Activity wird aufgerufen, wenn zu einer neuen Maske navigiert wird. Sie implementiert die Methoden onCancel(), onStop() und start(AcceptsOneWidget panel, EventBus eventBus). In der start-Methode werden alle Daten geladen, die für die Darstellung notwendig sind. Am Ende der Methode wird die Darstellungskomponente gesetzt und ist dadurch sichtbar.
Ein Place stellt einen „Platz“ in der Applikation dar. Er ist über den ActivityMapper immer an eine Activity gebunden, und diese wird dadurch via URL erreichbar. Ein Place erweitert den gwt.app.place.Place und implementiert einen PlaceTokenizer. Für jede Seite der Applikation wird also ein eindeutiges Token als String definiert, das beim Vor- und Zurücknavigieren in dem Browserverlauf wiederhergestellt werden kann.

Das MVP Pattern allgemein

Das Model View Controller (MVC) Pattern, aus dem das MVP Pattern hervorgeht, ist die Grundlage für die Kapselung der darstellenden Komponente (View), der Businesslogik (Model) und einer steuernden Komponente (Controller/Presenter). Durch diese Aufteilung werden Codewiederholungen vermieden, die Testbarkeit und Wiederverwendbarkeit des Sourcecodes erhöht und damit die Fehleranfälligkeit und der Wartungsaufwand minimiert. Der Vorteil von MVP im Vergleich zu MVC ist, dass die Darstellung völlig vom Model entkoppelt wird, indem ein Presenter dazwischen geschaltet wird (Abb. 1).

Abb. 1: MVP Pattern

Für große Applikationen, die mit GWT umgesetzt werden, empfiehlt Google das MVP Pattern und nennt dafür zwei Hauptgründe: Einerseits wird durch die Entkoppelung ermöglicht, dass viele Entwickler zeitgleich an der gleichen Codebasis arbeiten können. Andererseits wird die Anzahl der benötigten Testfälle minimiert. Wenn Businesslogik sowohl in der View als auch im Model abgebildet wird, erhöht sich der Testaufwand erheblich, vor allem, wenn die View viele Abhängigkeiten vom Model besitzt. Durch die Implementierung der Logik im Presenter konzentriert sich der Testaufwand auf diese Klassen, und das Mocken wird erheblich erleichtert.

Die View

In GWT kann die View aus einer Java-Klasse und einer XML-Datei bestehen. Letztere enthält die Struktur der Darstellung, während sich in der Java-Klasse die Methoden für das Verhalten des UI befinden. Beide Files werden mittels UI Binder aneinander gebunden. Grund für diese Trennung ist, dass ab GWT 2.0 die Darstellung an sich noch einmal separiert und gekapselt werden kann.
Styles für Elemente sind in einer oder mehreren CSS Files definiert, die mittels Resource Bundle eingebunden sind und in der Java-Klasse gesetzt werden. Alle Texte sind internationalisiert in mehreren Property Files abgelegt, die ebenfalls in der Java-Klasse den jeweiligen Widgets zugewiesen werden. Ähnlich wie in Java AWT oder Swing stellt GWT Event Handler wie zum Beispiel den FocusHandler, den ValueChangeHandler oder den KeyPressHandler zur Verfügung. In unseren Applikationen enthält die Java-Klasse also zumeist einen ClickHandler, setzt die CSS Styles und den internationalisierten Text aus den Property Files und vergibt eindeutige Widget IDs, die für die Selenium-Testfälle benötigt werden. Jedes Event wird an den Presenter weitergereicht. Weder die Java-Klasse (Listing 1) noch das XML (Listing 2) enthalten Businesslogik. Diese wird vollständig im Presenter implementiert.

public class SomeUserClass extends Composite {
  
  private static SomeUserClass UiBinder uiBinder = GWT .create(SomeUserClassUiBinder.class);
  private SomePresenter presenter;
  interface SomeUserClassUiBinder extends UiBinder<Widget, MdUserdata> {
  }
  @UiField Label lblFirstName, lblLastName;
  @UiField TextBox txtFirstName, txtLastName; 
  @UiField Button btnOk;
  
  public SomeUserClass() {
    initWidget(uiBinder.createAndBindUi(this));
    // Setting internationalized Strings into the Labels
    lblFirstName.setText(I18nUIMessages.INSTANCE.firstName()); 
    lblLastName.setText(I18nUIMessages.INSTANCE.lastName());
    // Setting styles to the TextBoxes
    txtLastName.setStyleName(Resources.INSTANCE.css().someStyle());
    txtFirstName.setStyleName(Resources.INSTANCE.css().someStyle2());
    // Setting WidgetIds for the selenium test cases
    ClientUtils.setWidgetId(txtFirstName, "txtFirstName", "SomeUserClass");
    ClientUtils.setWidgetId(txtLastName, "txtName", "SomeUserClass ");
    
    btnOk.addClickHandler(new ClickHandler() {
      @Override
      public void onClick(ClickEvent event) {
        presenter.userOkClicked(); 
      }
    });
  }
  
  // This method is called from the Activity within the start() method  
  public void init(String first, String last) {
    txtFirstName.setText(first);
    txtLastName.setText(last);
  }
  
  public void setPresenter(SomePresenter presenter) {
    this.presenter = presenter;
  }
  
}
<g:HTMLPanel>
  <g:VerticalPanel>
    <g:Grid borderWidth="0" width="100%" height="100%">
      <g:row>
        <g:customCell>
          <g:Label ui:field="lblFirstName"/>
        </g:customCell>
        <g:customCell>
          <g:TextBox  ui:field="txtFirstName"/>
        </g:customCell>
      </g:row>
      <g:row>
        <g:customCell>
          <g:Label ui:field="lblLastName"/>
        </g:customCell>
        <g:customCell>
          <g:TextBox  ui:field="txtLastName"/>
        </g:customCell>
      </g:row>
    </g:Grid>
    <g:FlowPanel>
      <g:Button ui:field = "btnOk"/>
    </g:FlowPanel>
  </g:VerticalPanel>
</g:HTMLPanel>

Das Model

Das Model enthält alle Entitäten und Daten der Applikation. Benötigt eine View Daten aus dem Model, holt sich der Presenter diese über die RequestFactory. Um Daten zwischen Server und Client hin und her zu senden, werden Proxys (DTOs) notwendig. Diese stellen Repräsentationen von Serverentitäten am Client dar. Sollen Daten durch Events im Client gespeichert werden (beispielsweise durch Neuanlage eines Benutzers), so ruft der Presenter ebenfalls über die RequestFactory die entsprechende Methode in der passenden Entity auf. Die RequestFactory ermöglicht objektorientierte Interaktion zwischen Presenter und Model. Über diese Factory können Methoden des Models aufgerufen und Rückgabewerte dieser Methoden empfangen werden. Der Datentransfer von View zu Model kann unter anderem mit dem Editor Framework realisiert werden.

Aufmacherbild: Raster version. Icon of shiny black tool box on white background Foto von Shutterstock / Urheberrecht: DVARG

[ header = Seite 2: Der Presenter ]

Der Presenter

Im Presenter vereinen sich das Activities und Places Framework sowie das MVP Pattern. Einen Teil des Presenters stellen Activities und Places dar. Der Presenter besteht in unseren Applikationen aus einer Activity und einem Place, die für jede UI-Maske vorhanden sind, sowie einer Presenter-Klasse. Die Presenter-Klasse wird nach Themengebieten und nicht pro View erstellt. Dort befindet sich die Businesslogik, die sich aus den Aktionen in der View (beispielsweise dem Sichern von Daten ins Model, der Validierung von Eingabedaten oder der Navigation zu einem andern Place in der Applikation) ergibt.
Im Konstruktor jeder Activity in unseren Applikationen wird die entsprechende Instanz der Darstellungskomponente von der Klasse Client Factory geholt. Stattdessen könnte auch ein Dependency Injection Framework (GIN für GWT) verwendet werden. Ferner holt sich die Activity ausschließlich jene Daten vom Model, die für die Darstellung, also den Start der View, notwendig sind. Die Activity implementiert außerdem die Logik, die für die Anzeige von UI-Feldern oder deren Befüllung verantwortlich ist. Werden beispielsweise Eingabefelder aufgrund von bestimmten Berechtigungen deaktiviert, wird diese deactivate-Methode, die in der View implementiert ist, von der Acitivity nach der entsprechenden Prüfung aufgerufen. Die Activity ist auch für die Auflösung von eventuell auftretenden Race Conditions zuständig. Diese können durch die asynchrone Verarbeitung der Calls zum Model auftreten.

public class SomeUserActivity extends AbstractActivity {  
  
  private SomeUserClass view;
  
  public SomeUserActivity(ClientFactory clientFactory) {
    // get view instance from ClientFactory
    view = clientFactory.getSomeUserClass();
  }
  
  @Override
  public void onCancel() {
    // do something when the start method has not yet replied to the 
    // callback, but the user has lost interest[6].
    
  }
  
  @Override
  public void onStop() {
    // do something when the Activity's widget has been removed from view. All event handlers it registered will have been removed before this method is called.[6]
  }

  // Within this method, model requests are fired and view methods to manage UI  elements are called; 
  @Override
  public void start(AcceptsOneWidget panel, EventBus eventBus){ 
    requests.someUserRequest().findSomeUser().fire(new  Receiver<SomeUserProxy>(){
      @Override
      public void onSuccess(SomeUserProxy response) {
        // Set data into the view
        view.init(response.getFirstName(), response.getLastName());
      }
      @Override
      public void onFailure(ServerFailure error) {
        log.info("Failed to load user!");
      }
    });
    panel.setWidget(view);
  }
}

Ein Place enthält, wie bereits beschrieben, immer einen Tokenizer, der für den Aufbau des URLs verantwortlich ist. Es können auch diverse Attribute über Places hinweg transportiert werden. Das kann beispielsweise bei Maskenverläufen notwendig werden, wo Variablenwerte über mehrere Places hinweg benötigt werden, um zum Beispiel Objekte zu spezifizieren, die vom Server nachgeladen werden müssen.

public class SomeUserPlace extends Place {
  
  public SomePlace() {
  }
  
  // Tokenizer
  public static class Tokenizer implements PlaceTokenizer<SomeUserPlace> {
    
    private RequestFactory requests;
    
    public Tokenizer(RequestFactory requests) {
      this.requests = requests;
    }
    
    @Override
    public SomeUserPlace getPlace(String token) {
      if("someUser".equals(token))  return new SomeUserPlace();
      else return null;
    }
    
    @Override
    public String getToken(SomeUserPlace place) {
      return "someUser";
    }
  }
}

Funktionsweise in Kombination mit MVP

Wenn im UI z. B. ein Button geklickt wird, wird von der View aus die entsprechende Presenter-Klasse aufgerufen. Im Presenter werden die Daten verarbeitet, die aus den Feldern der View übergeben werden. Außerdem wird die Navigation zu einem anderen Place veranlasst, sofern der Buttonklick einen Maskenwechsel mit sich bringen soll. Das geschieht mit der goTo-Methode des PlaceControllers. Der PlaceController feuert anschließend ein PlaceChangeEvent, auf das PlacesHistoryHandler und ActivityManager gleichermaßen reagieren. Der PlaceHistoryHandler updatet sich selbst mit dem aktuellen Place Token. Der ActivityManager ruft den entsprechenden ActivityMapper auf. Der ActivityMapper bestimmt die nächste Activity abhängig vom übergebenen Place. Anschließend wird die start-Methode in der Activity aufgerufen. Die Activity holt sich vom Model alle Daten, die zur Anzeige der aufrufendenDarstellungskomponente notwendig sind, und setzt die View (Abb. 2).

Abb. 2: Funktionsweise des Activities and Places Frameworks mit MVP

Die PresenterClass wäre nicht unbedingt nötig, denn diese könnte auch vollständig in den stop()– oder cancel()– Methoden der Activities implementiert werden. Allerdings haben wir in unseren Applikationen diese zusätzliche Schicht eingezogen, um themenverwandte Aufrufe an einer Stelle und nicht in jeder Activity separat zu implementieren.
Das Activities and Places Framework kann unabhängig von MVP verwendet werden, genauso wie nach MVP ohne Activities and Places Framework entwickelt werden kann. Durch die Integration des Activities and Places Frameworks in GWT kann allerdings empfohlen werden, dieses in Kombination mit dem MVP Pattern zu implementieren.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -