Implementierung eines mobilen Clients mit jQuery Mobile

Vom klassischen Webinterface zur Web-App
Kommentare

Wie entwickelt man für eine CRM-Softwareanwendung mit einem klassischen Webinterface möglichst effektiv einen mobilen Client, der aufgrund der Flexibilität der Software Datenbankinhalte jeder Art weitgehend generisch darstellen können muss? Neben einem Blick hinter die Kulissen soll der folgende Artikel einige implementierungstechnische Aspekte eines solchen Unterfangens vorstellen, die nützlich für die Entwicklung eigener mobiler Datenbankanwendungen sein könnten.

Mit der CRM-Software „combit Relationship Manager“ erhalten Unternehmen jeder Branche und Größe Unterstützung bei Prozessen der Kundenakquise und -bindung. Dabei steht die CRM-Lösung ganz im Zeichen der Flexibilität, denn Oberfläche, Datenbankstruktur und Relationen können vom Anwender individuell gemäß den eigenen Anforderungen definiert werden. Neben den klassischen Windows-Clients gibt es für den Zugriff per Webbrowser einen auf ASP.NET-Technologie basierenden cRM.WebAccess Server (Abb. 1). Der cRM.WebAccess Server wird als Anwendung im IIS gehostet. Er stellt eine Rich-Internet-Application-Oberfläche mit ASP.NET-Steuerelementen zur Verfügung und greift per AJAX auf die ebenfalls bereitgestellten Daten- und UI-Services zu (Abb. 2). Uns war relativ schnell klar, dass eine derartig „reiche“ Oberfläche nicht den Anforderungen eines Smartphones gerecht wird. Aus diesem Grund wollten wir einen speziell auf die Benutzung per Smartphone ausgelegten Client entwickeln.

Abb.1: Grafische Veranschaulichung des cRM.WebAccess Server

Abb. 2: Rich-Internet-Application-Oberfläche

Anforderung

Angesichts der hohen Dynamik des Smartphonemarktes wollten wir nach Möglichkeit eine Smartphone-Betriebssystem-übergreifende Lösung schaffen. Bei den Kunden sollte sich die Lösung möglichst einfach installieren bzw. verteilen und warten lassen. Die Bedienung sollte ebenfalls möglichst einfach sein. Aus Aufwandsgründen wollten wir auf jeden Fall die bestehende und erprobte cRM.WebAccess-Service-Infrastruktur verwenden. Das lokale Speichern von Kundenadressen auf dem mobilen Endgerät sollte aus Sicherheitsgründen vermieden werden, stattdessen sollte ein Onlinezugriff auf die zentralen Kundendaten erfolgen. Der mobile Client musste der jeweiligen vom Kunden individuell angepassten CRM-Lösung gerecht werden können.

Web-App mit jQuery Mobile

Eine Lösung, die die vorgenannten Anforderungen weitestgehend erfüllte, war eine Web-App: Dabei handelt es sich streng genommen um eine normale Webseite, die gezielt auf die Besonderheiten einer Smartphoneoberfläche abgestimmt ist und sich optisch (fast) als native App präsentiert. Frameworks, die bei der Implementierung helfen, gibt es mittlerweile einige, wir entschieden uns relativ schnell für jQuery Mobile (jQM). Der WebAccess Server sollte also eine clientspezifische Weiche bekommen und für Smartphones eine separate mobile Oberfläche bereitstellen (Abb. 3).

Abb. 3: Der WebAccess Server mit clientspezifischer Weiche

App-Feeling erzeugen

Um möglichst wie eine native App auszusehen, braucht auch die Web-App ein Icon. Installiert wird die Web-App ganz einfach, indem auf dem Home-Bildschirm der URL des WebAccess Servers als Lesezeichen gespeichert wird (Abb. 4).

Abb. 4: URL als Lesezeichen

Das zu verwendende Symbol wird in der <head>-Sektion der Webseite angegeben, außerdem setzen wir den Viewport so, dass er ein Skalieren des Inhalts durch den Anwender erlaubt, aber initiell in jedem Fall die maximale Breite des Gerätes ausnutzt:

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"><title>cRM.Mobile</title>
    <link rel="apple-touch-icon" href="../images/apple-touch-icon.png"> 

Exkurs

„Vorsicht Falle!“ beim „apple-mobile-web-app-capable“-Meta-Tag. Im Zusammenhand mit Web-Apps wird oft das Setzen von <meta name=“apple-mobile-web-app-capable“ content=“yes“ /> propagiert. Doch so praktisch das Meta-Tag auch erscheint, immerhin bekommt man dadurch auf dem iPhone eine tolle Vollbildanwendung ohne die Browser-Schaltflächenzeile, macht ein damit verbundener Bug es für Anwendungen mit Cookie-basierter SessionId und klassischen Authentifizierungsmechanismen, wie in unserem Fall, leider unbrauchbar. Denn wird die Web-App durch einen Task-Wechsel in den Hintergrund gebracht (z. B. durch einen Anruf oder das Wechseln zu einer anderen App) und dann wieder nach vorne geholt, so verliert die Sitzung in dem Augenblick alle ihre Session-Cookies und auf dem Webserver wird beim gleichzeitig forcierten Reload eine komplett neue Sitzung inklusive neuer SessionId initiiert. Vom ärgerlichen Verlust potenzieller noch nicht gespeicherter Eingaben einmal ganz abgesehen, führt dies zwangsläufig dazu, dass sich der Benutzer anstatt auf dem zuletzt angesehenen Kundendatensatz nun auf der Anwendungs-Login-Seite wiederfindet. Apple ist dieser Bug bekannt, eine Aussage ob und wann er behoben wird, gibt es aber nicht. Aus diesem Grund verzichten wir derzeit auf den Einsatz dieses Tags.

Browser-Adresszeile verstecken

Um das App-Feeling weiter zu verbessern, blenden wir die Adresszeile des Browsers JavaScript-seitig aus (Listing 1). Dazu bedienen wir uns eines Tricks: Wir scrollen die Seite vertikal um einen Pixel nach oben, was den Browser veranlasst, die Seite dann um die komplette Höhe der Adresszeile nach oben zu verschieben und letztere damit zu verstecken. Das Ganze darf nur stattfinden, wenn nicht sowieso ein Seiten-Anchor angesprungen werden soll (window.location.hash), was bei Verwendung von jQM allerdings von Haus aus nicht möglich ist. Ein sehr gut geeignetes Code-Snippet ist auf den mobile.tutsplus-Seiten zu finden.

function hideAddressBar()
{
    if (!window.location.hash)
    {
        if (document.height <= (window.outerHeight + 10))
        {
            document.body.style.height = (window.outerHeight + 50) + 'px';
            setTimeout(function() { window.scrollTo(0, 1); }, 50);
        }
        else
        {
            setTimeout(function() { window.scrollTo(0, 1); }, 0);
        }
    }
}
$(document).bind("orientationchange", function(event, data) { hideAddressBar(); });
$(document).bind("load", function(event, data) { hideAddressBar(); });

Falls eine Seite nicht hoch genug ist, dass sie überhaupt scrollbar wäre, wird sie kurzerhand auf die notwendige Höhe „verlängert“. Die hideAddressBar-Funktion funktioniert auf allen mobilen Geräten – ausgenommen dem Windows Phone, weil dort die Adresszeile aus unerfindlichen Gründen unten anstatt oben ist und sich leider nicht „wegscrollen“ lässt.

Generische Datenbankoberfläche mit jQM

Wie eingangs erwähnt, sind uns die darzustellenden Entitäten mit ihren Eigenschaften und Relationen zur Implementierungszeit aufgrund der komplett freien Anpassbarkeit durch den Kunden nicht bekannt. Dies bedeutet, dass es nicht möglich war, eine Oberfläche zur Darstellung von „Firmen“, „Kontakten“, „Produkten“ statisch zu programmieren. Also muss die mobile Oberfläche zwingend generisch gehalten werden, was im Ergebnis zu der Seitenstruktur in Abbildung 5 führt:

  • Darstellung von Datensätzen einer Tabelle in Listenform (viewlist.aspx)
  • Darstellung eines Datensatz in Detailform (viewdetails.aspx)
  • Eingabemaske zur Bearbeitung eines Datensatzes (viewedit.aspx)

 

Abb. 5: Seitenstrukturen

Diese Seiten werden über den jQM-Page-Mechanismus per AJAX dynamisch in die Web-App geladen und in das DOM injiziert. Der Anwender navigiert dann je nach Kontext zwischen diesen Seiten hin und her. Nebenbei: Die Verwendung von .aspx– anstelle von .html-Seiten ermöglicht uns noch eine gewisse Dynamisierung der Seiteninhalte (z. B. Lokalisierung der Texte), ist für das Grundkonzept jedoch nicht entscheidend. Die Information, welche Tabelle (Entität) jetzt jeweils konkret dargestellt werden soll, wird über URL-Parameter realisiert (Abb. 6). Dies hat gleich mehrere Vorteile: Zum einen kann der JavaScript Code zur Seiteninitialisierung (pageinit-Event) bzw. -darstellung (pageshow-Event) per Querystring-Auswertung auf einfache Weise den aktuellen Kontext (Entität, ggf. RecordID u.ä.) ermitteln und z. B. in den Web Service Calls zur Datenversorgung weiterverwenden. Zum anderen können so einfach Hyperlinks z. B. für den Wechsel in die Detaildarstellung eines Datensatzes verwendet werden, da alle Informationen direkt im Link hinterlegbar sind. Des Weiteren kommt durch die Hinterlegung des kompletten Kontextes im URL die Web-App auch mit „Aktualisieren“ und einer „Vor“- bzw. „Zurück“-Navigation per Browser-App-Menü bzw. Smartphonetasten zurecht, was bei AJAX-Anwendungen ansonsten immer wieder ein heikler Punkt ist.

Abb. 6: Realisierung über URL-Parameter

Übersichtsliste ViewList.aspx

Die Seite aus Listing 2 besteht aus einem Kopfbereich, der den Seitentitel (wird im pageShow-Event durch den konkreten Entity-Namen ersetzt) und Navigationsschaltflächen bereitstellt, und einem Datenbereich, der eine Suchfeld und die eigentlichen Datensatzliste enthält. Über einen URL-Parameter Entity wird gesteuert, aus welcher Tabelle die Datensätze geholt werden sollen. Der Datenservice liefert dabei je Datensatz nur die Felder, die der Anwender in der Übersichtsliste sehen möchte. Abbildung 7 veranschaulicht das.

<!-- page datalist -->
<div data-role="page" id="PDataList" data-title="" data-theme="b" data-content-theme="c" data-dom-cache="true">
    <div data-role="header" data-position="fixed" data-theme="a">
        <a onclick="history.back();" href="" data-icon="back" data-iconpos="notext" data-direction="reverse">Zurück</a>
        <div data-role="controlgroup" data-type="horizontal" class="ui-btn-right" style="margin-top:0px">
            <a onclick="beginNew();" href="" data-role="button" data-icon="plus" data-iconpos="notext">Neu</a>
            <a href="#PViews" data-role="button" data-icon="home" data-iconpos="notext">Start</a>
        </div>
        <h1>cRM.Mobile</h1>
    </div>
    <div id="searchArea">
         <input type="search" name="search" id="searchInput" value="" data-theme="c" />
    </div>
    <div data-role="content" data-theme="c" >
        <ul id="dataList" data-role="listview" data-theme="c" data-divider-theme="b"></ul>
    </div>
</div>

Abb.7: Angeforderte Übersichtsliste

Dynamisches data fetching beim Scrollen

Um auch bei geringer Bandbreite eine gute Responsiveness zu erreichen, werden lediglich die ersten 20 Datensätze geholt. Erst wenn der Anwender nach unten scrollt, wird dann dynamisch der nächste „Bulk“ geholt usw. Hierzu benutzen wir den $(window).scroll-Event, mit dem wir dann prüfen, ob die Scroll-Position des Dokumentes bereits „fast“ ganz unten angelangt ist, und lösen dann das Holen des nächsten Bulks aus. Mit dem Delta, ab dem dies stattfinden soll, musste ein wenig experimentiert werden, damit dies bei allen mobilen Browsern gleich gut funktioniert. So hat bspw. der Windows Phone IE seine Location-Bar unten anstatt oben, eine aktivierte Debug-Konsole im Safari Browser „kostet“ bspw. 10 Pixel extra usw. Wir haben mit 110 Pixeln als Delta gute Erfahrungen gemacht. Da ein Browser u. U. beim Scrollen gleich mehrere scroll-Events auslöst, muss unter dem Performance-Gesichtspunkt der Code noch derart abgesichert werden, dass er nur ausgeführt wird, wenn nicht sowieso gerade ein Bulk geholt wird. Beim initiellen Darstellen der Seite sowie beim Erhalt des Bulks wird scrollEventPending wieder zurückgesetzt (Listing 3).

if ($(document).height() - $(window).height() - $(window).scrollTop() <= 110)
{
    if (!scrollEventPending)
    {
        scrollEventPending = true;
        //get the next bulk of records:
        //...
    }
}

Wenn ein Bulk an Datensätzen angekommen ist, werden diese in HTML-Code „gegossen“ (inklusive einem Hyperlink für den Wechsel in die Detailansicht) und per

$('#dataList').append(line);

an die schon vorhandene Liste angehängt. Selbige wird dann per

$("#dataList").listview("refresh");

aktualisiert.

Aufmacherbild: Photo of a rotary card index with Contact Us von Shutterstock / Urheberrecht: RTimages

[ header = DOM-Caching bringt Performance]

DOM-Caching bringt Performance

Wenn der Anwender zwischen Übersichtsliste-, Detailansicht- und Eingabeformular-Seiten hin und her wechselt, entlädt jQM standardmäßig jedes Mal die zuletzt dargestellte Seite aus dem DOM. Dies führt dazu, dass wenn sich der Anwender ausgehend von der Übersichtsliste kurz ein Datensatz im Detail ansieht und dann wieder zurück in die Übersichtsliste wechselt, wieder die ersten 20 Datensätze über den Datenservice neu geholt und wieder neu in die Liste gepackt werden müssen. Hat der Anwender zuvor weiter nach unten gescrollt, sodass der betrachtete Datensatz gar nicht im ersten Bulk liegt, dann müssten gegebenenfalls sogar noch weitere Bulks geholt werden. Um diesen potenziellen Flaschenhals zu vermeiden, arbeiten wir in der Übersichtsliste mit dem jQM DOM-Caching-Feature:

<div data-role="page" id="PDataList" data-title="" data-theme="b" data-content-theme="c" data-dom-cache="true">

Dies bedeutet, dass die #PDatalist-Seite im DOM erhalten bleibt, selbst wenn die Seiten gewechselt werden. Dadurch stehen bei der Rückkehr zu ihr alle bisher geholten Datensätze sofort zur Verfügung, es wird kein Request über das Netzwerk notwendig. Allerdings hat der DOM-Cache eine kleine Tücke: Er benutzt als Key nicht nur die Page-ID (#PDataList), sondern auch den aktuellen Querystring. Dies hat zur Konsequenz, dass wenn von „Firmen“ zu den „Kontakten“ gewechselt wird, die bereits im DOM-Cache befindliche #PDataList-Seite (mit Entity=Firmen als URL-Parameter) nicht recycelt wird, sondern zusätzlich eine weitere #PDataList-Seite (nämlich mit dem Entity=Kontakte-URL-Parameter) in das DOM eingefügt wird. Dies ist ein Problem, weil dadurch dann ihre ID und die aller ihrer Unterelemente im DOM mehrfach vorkommen, denn es gibt ja nur diese eine einzige generische ViewList.aspx. Wir müssen uns also die aktuelle Übersichtsliste inklusive Querystring laufend merken (Listing 4). Und wenn zu einer Übersichtsliste für eine andere Entität als bislang gewechselt werden soll, müssen wir die im DOM befindliche bisherige #PDatalist explizit „zu Fuß“ aus dem DOM entfernen (Listing 5). Dadurch haben wir zu jeder Zeit nur eine #PDataList im DOM, und diese behält alle bereits abgerufenen Daten im DOM, solange die Entität nicht gewechselt wurde.

$('#PDataList').live('pagebeforehide', function(event, data)
{
        __lastDataListUrl = $("#PDataList").attr("data-url");
});
$(document).bind("pagebeforechange", function(event, data)
{
    if (typeof data.toPage === "string")
    {
        var toPageUrl = $.mobile.path.parseUrl(data.toPage);
        var toPage = toPageUrl.pathname + toPageUrl.search;
        //if we need to recycle the PDataList element, then remove the old one from DOM cache:
        if (__lastDataListUrl && toPage.indexOf('ViewList.aspx') >= 0 && __lastDataListUrl !== toPage)
        {
            $("div[data-url='" + __lastDataListUrl + "']").remove();

Detailansicht

Die Seite aus Listing 6 (Abb. 8)besteht aus einem Kopfbereich, der den Seitentitel (wird im pageShow-Event um den konkreten Entity-Namen ergänzt) und Navigationsschaltflächen bereitstellt, einem Datenbereich, der den Datensatz darstellt, und einem Fußbereich, der weitere Befehlsschaltflächen enthält. Über einen UI-Web-Service-Aufruf wird die zugehörige optische Darstellung, also quasi das „Kochrezept“, welche Felder überhaupt in welcher Reihenfolge dargestellt werden sollen, abgefragt, und dann der per URL-Parameter angeforderte Datensatz per Datenservice geholt. Beide Informationen werden dann kombiniert und die #detailList entsprechend gefüllt.

<!-- page details -->
<div data-role="page" id="PDetails" data-title="" data-theme="b" data-content-theme="c"> 
    <div data-role="header" data-position="fixed" data-theme="a">
        <a onclick="history.back();" href="" data-icon="back" data-iconpos="notext" data-direction="reverse">Zurück</a>
        <a href="#PViews" data-icon="home" data-iconpos="notext" class="ui-btn-right" data-direction="reverse">Start</a>
        <h1>Details</h1>
    </div>
    <div data-role="content" id="detailcontent" data-theme="b">
        <ul id="detailList" data-role="listview"  data-inset="true" data-theme="c" data-divider-theme="a" ></ul>
    </div>
    <div data-role="footer" data-position="fixed" data-theme="a">
        <div data-role="navbar">
            <ul>
                <li><a id="btnMenu" onclick="onShowMenu();" href="#" data-icon="menu"></a></li>
                <li><a id="btnSMS" onclick="onSmsMenu();" href="#" data-icon="sms"></a></li>
                <li><a id="btnMail" onclick="onMailMenu();" href="#" data-icon="mail"></a></li>
                <li><a id="btnPhone" onclick="onPhoneMenu();" href="#" data-icon="phone"></a></li>
            </ul>
        </div>
    </div>
</div>

Abb. 8:Datensatz per Datenservice

Clever Hyperlinks

In Kommunikationsfeldern werden die Inhalte mit Hyperlinks hinterlegt (Abb. 9). Über die Präfixe „phone:“, „sms:“ und „mailto:“ werden dann automatisch die Standard-Apps für die jeweiligen Aktionen gestartet. Zu beachten ist, dass die Telefonnummer hinter dem Link je nach Smartphone keine Sonderzeichen wie / enthalten darf, ebenso unterstützt nicht jedes Smartphone den „sms:“-Präfix. Das Verzweigen in andere mit dem Datensatz verknüpfte Entitäten geschieht ebenfalls über Hyperlinks, die auf die Übersichtsliste mit entsprechendem Entitäts- und Fremdschlüssel-Parameter verweisen (Abb. 10).

Abb. 9: Kommunikationsfelder mit Hyperlinks

Abb. 10: Übersichtsliste mit entsprechendem Entitäts- und Fremdschlüssel-Parameter

Eingabeformular

Für eine Akzeptanz einer Lösung muss unserer Erfahrung nach auch die Pflege (sprich: Eingabe) von Daten per Smartphone für den Anwender möglichst einfach sein. Hierzu haben wir die Eingabeelemente mit einem zum Datentyp passendem HTML5 Input Type versehen, der dem Smartphone die Anzeige einer inhaltstypspezifischen Tastatur ermöglicht. HTML5 unterstützt hierzu die Typen tel, time, date, datetime, url, email, number und checkbox als speziellere Alternative zum Typ text. Ein Beispiel für <input type=’datetime‘ … /> auf dem iPhone ist in Abbildung 11 zu sehen. Nach dem erfolgreichen Speichern der Daten per Web Service wird anschließend JavaScript-seitig einfach per history.back() zur letzten Ansicht zurück gewechselt.

Abb. 11: Beispiel auf dem iPhone

Ausblick

Neben diversen funktionalen Erweiterungen der mobilen Oberfläche möchten wir nach Erscheinen der kommenden Version 18 unserer Reporting-Entwicklerkomponente List & Label, das dann dort neu verfügbare jQuery-Mobile-Exportformat für Berichte nutzen, sodass darüber dann auch CRM-Auswertungen und Statistiken direkt auf dem mobilen Gerät zur Verfügung stehen.

Fazit

Mit jQuery Mobile konnten wir bei einer bereits vorhandenen Datenservice-Architektur mit relativ geringem Aufwand eine mobile Datenbankoberfläche als Web-App implementieren, die auf iPhone, Android, BlackBerry und Windows Phone gleichermaßen funktioniert und dabei fast wie eine native Mobile-Anwendung aussieht. Wer einmal selber ausprobieren möchte, wie sich die Web-App jetzt als Endergebnis „anfühlt“, sei hierauf verwiesen.


Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -