Performance im UI – serverseitige SuggestBox

Für den Benutzer einer Applikation ist die Performance des UIs einer der wesentlichsten Punkte, über den er Qualität des Produkts wahrnimmt. Kaum etwas ärgert so sehr, als wenn jeder Arbeitsschritt spürbare Verzögerungen verursacht.

Einen Weg, um eine GWT-SuggestBox auch bei einer hohen Anzahl an möglichen Werten performant – und skalierbar – zu halten, möchte ich heute vorstellen.

Die von GWT gelieferte SuggestBox bezieht die im UI angezeigten Werte aus dem dazugehörigen SuggestOracle. Um eine gute Skalierbarkeit und Performance auch bei tausenden grundsätzlich möglichen Werten garantieren zu können, muss man sicherstellen, dass an den Client jeweils nur eine begrenzte Anzahl an Werten geliefert wird. Sonst kann es dazu kommen, dass entweder das UI für den Benutzer “einfriert” oder, noch schlimmer, z.B. in älteren Versionen des Internet Explorers eine Rückfrage an den Benutzer erfolgt, ob das Skript weiter ausgeführt werden soll:

Browserwarnung

Die von mir entwickelte Implementierung von SuggestBox, die für Vorschläge an den Benutzer einen Servercall durchführt, beruht auf einer Implementierung von Bess Siegal.

Im UI sieht die SuggestBox so aus:

Dabei sind clientseitig nur die im Dropdown angezeigten Werte verfügbar, mit Klick auf die Pfeile werden (alphabetisch sortiert) weitere Suchergebnisse abgerufen.

Die Kernteile sind:

  • ein Timer, damit nicht während der Benutzer noch tippt, unnötige Servercalls abgesetzt werden und damit der Server überlastet wird,
  • die Möglichkeit, flexibel einen bestimmten Servercall anzugeben, der für die Vorschläge abgesetzt werden soll,
  • die Verarbeitung der Ergebnisse,
  • und last but not least die Anzeige im UI.

Eine SuggestBox mit einem zugewiesenen SuggestOracle ruft für das Abrufen der Vorschläge folgende Methoden auf:

  • public void requestSuggestions(Request request, Callback callback) – bei Eingabe eines Texts
  • public void requestDefaultSuggestions(Request request, Callback callback) – “Default”, d.h. keine Eingabe

Der folgende Aufruf ist direkt aus der oben genannten Implementierung übernommen, als Delay habe ich 300ms gesetzt. Damit wird bei flüssigem Tippen des Benutzers erst beim Abschluss ein Request gesendet.

@Override 
public void requestSuggestions(Request request, Callback callback) {
this.request = request;
this.callback = callback;

//reset the indexes (b/c NEXT and PREV call getSuggestions directly)
resetPageIndices();

//If the user keeps triggering this event (e.g., keeps typing), cancel and restart the timer
timer.cancel();
timer.schedule(TIMER_DELAY_MILLIS);
}

Der Timer wird über die Methode init() gesetzt und ruft die Methode auf, die dann den Request an den Server absendet.

public void init(final SuggestBox suggestBox) { 
this.box = suggestBox;
timer = new Timer() {
@Override
public void run() {
retrieveSuggestionsFromServer();
}
};
...
}

Wichtig für den implementierenden Entwickler ist die Möglichkeit, einen Servercall selbst festzulegen, damit das SuggestOracle wirklich generisch verwendbar ist. 

Dabei wird über die UI-Komponente (oder, bei Trennung von UI und Presenter, der Presenter) im SuggestOracle der Call gesetzt:

company.setCompanyServerSuggestRequest(new ServerSuggestionRequest() {
@Override
public void findBy(String searchValue, int startIndex, int endIndex, ServerSuggestCallback callback) {
requestFactory.companyRequest().findCompanies(id, searchValue, startIndex, endIndex).fire(callback);
}
});


Dieser Request an den Server wird vom SuggestOracle entweder über den Timer aufgerufen, oder, bei Auswahl der “Spezialwerte” NEXT und PREVIOUS, über den SelectionHandler.

private void retrieveSuggestionsFromServer() { 
String searchValue = ClientUtils.trimToEmpty(request.getQuery());
if (suggRequest != null) {
suggRequest.findBy(searchValue, startIndex, startIndex+suggestionSize, new ServerSuggestCallback());
}
}

Nach Abschluss des (asynchronen) Request wird der angegebene ServerSuggestCallback aufgerufen, dieser kümmert sich um die Anzeige der Vorschläge. Wir verwenden bei uns die GWT-RequestFactory (im Gegensatz zu der Implementierung von Siegal, die mit REST arbeitet).

public class ServerSuggestCallback extends Receiver<ServerSuggestResultSetProxy> { 

@Override
public void onSuccess(ServerSuggestResultSetProxy result) {
...
if(result.getResults().isEmpty()) {
OptionSuggestion noResults = new OptionSuggestion(NO_RESULTS, getQuery());
suggs.add(noResults);
}

//if not at the first page, show PREVIOUS
if (startIndex > 0) {
OptionSuggestion prev = new OptionSuggestion(PREVIOUS, getQuery());
suggs.add(prev);
}
// show the suggestions
List<String> keys = result.getResults().getKeys();
List<String> values = result.getResults().getValues();
for (int i = 0; i< keys.size(); i++) {
suggs.add(new OptionSuggestion(keys.get(i), values.get(i)));
}
//if there are more pages, show NEXT
if (startIndex+suggestionSize < totalSize) {
OptionSuggestion next = new OptionSuggestion(NEXT, getQuery());
suggs.add(next);
}

response.setSuggestions(suggs);
callback.onSuggestionsReady(request, response);
}
@Override public void onFailure(ServerFailure error) {
// Fehlerbehandlung
}
}

Bei Auswahl der Pfeilsymbole NEXT und PREVIOUS soll für den Benutzer der von ihm eingegebene Wert übernommen werden, hierbei muss man ein wenig tricksen:

private class OptionSuggestion implements SuggestOracle.Suggestion { 
// Membervariablen
public OptionSuggestion(String key, String value) {
// Die SuggestBox hat einen default onSelect-Handler, der zwingend die Selection ins Feld setzt. Diesen kann man auch nicht ersetzen.
// Bei Auswahl vom Pfeil wäre ein Pfeil allerdings für den Benutzer verwirrend, daher verwenden wir den vom Benutzer eingegebenen Wert für die Anzeige.
// Im SelectionHandler wird dann bei Auswahl eines der Pfeile ohnehin speziell reagiert
if (SPECIAL_KEYS.contains(key)) {
this.display = key;
} else {
this.display = value;
}
...
}

Mit diesem SuggestOracle kann man fast beliebig skalierbar für den Benutzer eine moderne und angenehm zu bedienende Komponente anbieten. Der einzige größere Nachteil gegenüber einem SuggestOracle, welches alle möglichen Vorschläge clientseitig speichert, ist die Komplexität des Abrufs auf Serverseite: Hier muss nämlich je nach Anforderung evtl. ein recht komplexes SQL verfasst werden, da die Limitierung der Anzahl der Ergebnisse auf Datenbankebene erfolgen muss.

Lisi Blümelhuber
(Software Developer)