In der ersten Version handelt es sich um eine auf- und zuklappbare Liste mit Blumennamen. Realisiert wird dies mit dem JQuery-Widget collapsible set. Klickt man auf eine Blume, dann öffnet sich der Detailbereich mit zusätzlichen Informationen und einem Link zur Kulturanleitung im PDF-Format.
Ausserdem soll man mit zwei Schaltflächen im Fussbereich zwischen deutschen und lateinischen Blumennamen wechseln können. Ein Suchfeld hilft, nach Teilen des deutschen oder lateinischen Namens zu suchen. Die Seite soll möglichst deklarativ ohne Programmierung mit Code behind erstellt werden.
Da es bei einer Mobile-Seite von den Tarifen her wichtig ist, dass keine unnötigen Bytes geladen werden, baue ich die Seite mit AJAX so um, dass der Detailinhalt erst geladen wird, wenn man auf einen Blumennamen klickt.
- Unter http://jquerymobile.com/themeroller/ ein eigenes Theme mit den gewünschten Farben und Stilelementen erstellen und als ZIP-Datei herunterladen
- Den Ordner themes aus der ZIP-Datei auspacken und im gewünschten Website als Unterordner des Root-Verzeichnisses speichern
- Den Website im Visual Web Developer Express öffnen
- Ein Unterverzeichnis /mobile/ erstellen und darin eine neue Masterseite erstellen
- Im Header der Masterseite das eigene Theme und die JQuery-Libraries einbinden
<link rel="stylesheet" href="/themes/Ecotronics.min.css" /> <link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.1/jquery.mobile.structure-1.3.1.min.css" /> <script src="http://code.jquery.com/jquery-1.9.1.min.js"> </script> <script src="http://code.jquery.com/mobile/1.3.1/jquery.mobile-1.3.1.min.js"> </script>
- In die Masterseite Header, Footer und Platzhalter für den Content-Teil für die Mobile-Seite einfügen
<div data-role="page" data-theme="a"> <div data-role="header" data-position="fixed"> <a href="Indexm.aspx" data-role="button" data-icon="home" data-iconpos="notext" data-inline="true">Startseite</a> <h1>Blumenfinder</h1> </div> <form id="form1" runat="server"> <div data-role="content"> <asp:ContentPlaceHolder id="ContentPlaceHolder1" runat="server"> </asp:ContentPlaceHolder> </div> </form> <div data-role="footer" class="ui-bar" data-position="fixed"> <a href="?l=l" data-role="button" data-mini="true">Lateinisch</a> <a href="?l=d" data-role="button" data-mini="true">Deutsch</a> </div> </div>
- Ich gehe davon aus, dass die Datenbank, aus welcher Information angezeigt werden soll, bereits existiert und auf dem Webserver publizert ist. Für das vorliegende Projekt handelt es sich um einen Auszug aus einer lokalen Datenbank, so dass die gewünschten Informationen aus einer einzigen Tabelle stammen. Falls noch nicht vorhanden, trägt man jetzt den Connection String in Web.config unter dem Tag <Connection Strings> ein
<add name="myConnectionString" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DATADIRECTORY|\myDatabaseName.mdb" providerName="System.Data.OleDb">
- Nun erzeugt man eine ASPX-Seite. Die aufklappbare Liste besteht aus einem alles umspannenden Div-Tag mit einem Attribut data-role=“collapsible-set“. Den Inhalt der Liste erzeugen wir mit einem Repeater, da nur diese Komponente Zeilen aus einer Datenbank in beliebigem HTML rendern kann. Die einzelnen Elemente der Liste sind Titel-Elemente, gekennzeichnet mit dem Tag <h3> und ein Paragraph (<p>-Tag) mit dem aufklappbaren Detailbereich.
- Vor der eigentlichen Liste steht eine Textbox für die Suche
<asp:TextBox ID="txtName" runat="server" AutoPostBack="True"></asp:TextBox>
- Und natürlich benötigen wir eine Datasource-Komponente, welche mit SQL die gewünschten Datensätze aus der Datenbank holt. Damit sie die volle Liste liefert, solange nichts im Suchfeld steht, verwendet das SQL LIKE und ist parametrisiert, wobei die Parameter aus dem Suchfeld stammen.
Alle diese Elemente zusammen ergeben im Body der aspx-Seite den folgenden Code:
<asp:TextBox ID="txtName" runat="server" AutoPostBack="True"></asp:TextBox><br /> <asp:SqlDataSource ID="dsKeimverhalten" runat="server" ConnectionString="<%$ ConnectionStrings:myConnectionString%>" ProviderName="<%$ ConnectionStrings:myConnectionString.ProviderName %>" SelectCommand="SELECT [lateinischername], [name_d], [pflanzanleitung], [bildpfad], [bildurl], [bildautoren], [bildquelle], [keimlingBildpfad], [keimlingAutoren], [keimlingUrl] FROM [myTableName] WHERE ([name_d] LIKE '%' + ? + '%' OR [lateinischername] LIKE '%' + ? + '%' ) ORDER BY [lateinischername]"> <SelectParameters> <asp:ControlParameter ControlID="txtName" DefaultValue="%" Name="deutschername" PropertyName="Text" Type="String" /> <asp:ControlParameter ControlID="txtName" DefaultValue="%" Name="lateinischername" PropertyName="Text" Type="String" /> </SelectParameters> </asp:SqlDataSource> <div data-role="collapsible-set"> <asp:Repeater ID="repKeimverhalten" runat="server" DataSourceID="dsKeimverhalten"> <ItemTemplate> <div id="myCollapsible" data-role="collapsible" data-collapsed="true"> <h3> <asp:Label ID="blumenLateinischerName" runat="server" Text='<%#Eval("lateinischername")%>'> </asp:Label></h3> <p id="blumenDetail"> <asp:Label ID="Label2" runat="server" Text='<%#Eval("name_d")%>'> </asp:Label><br /> <asp:HyperLink ID="linkPflanzanleitung" runat="server" NavigateUrl='<%# "/pdfs/" + Eval("pflanzanleitung") %>' Text='<%# Eval("pflanzanleitung") %>' Target="_blank" Visible='<%#!DBNull.Value.Equals(Eval("pflanzanleitung"))%>'> </asp:HyperLink> <br /> <asp:Image ID="imgBlumenK" runat="server" ImageUrl='<%# imgRoot + "/imgblumenk/" + Eval("bildpfad") %>' Visible='<%#!DBNull.Value.Equals(Eval("bildpfad"))%>' Height="80" /> </p> </div> </ItemTemplate> </asp:Repeater> </div> </div>
Damit haben wir zwar bereits eine funktionierende Webapplikation fürs Handy, aber vom ersten Request an wird alles geladen, auch die zugeklappten Äste der Liste mit allen Texten und Bildern. Für all jene, die auf dem Handy keine Datenflatrate haben, ist das natürlich unakzeptabel. Jetzt kommt unsere Ausbaustufe ins Spiel: Der Detailbereich soll erst geladen werden, wenn jemand auf einen Titel klickt und den Detailbereich effektiv öffnet.
Ausbau mit AJAX
In ASP.NET gibt es mehrere Möglichkeiten, mit AJAX zu arbeiten:
- Für einfache Seiten gibt es die ASP.NET-Komponente UpdatePanel. Allerdings hatte ich meine Zweifel, dass diese sehr eingeschränkte Komponente sich zusammen mit JQuery Mobile einsetzen lässt.
- Die zweite Möglichkeit besteht darin, in CSharp einen generischen Handler zu programmieren, der vom AJAX-Request aufgerufen wird. Diese Methode habe ich andernorts verwendet, um die Daten für ein Autocomplete-Feld zu erzeugen. Für diesen Anwendungsfall war das sinnvoll, da eine reine Datenliste geliefert wird. Im aktuellen Beispiel möchten wir jedoch mit AJAX einen Teil einer Seite mit HTML-Tags für Links und Bilder zurückliefern. Dies alles aus CSharp heraus zu erzeugen, ist zwar möglich, aber sehr umständlich. Wer in den Urzeiten vor ASP und JSP je Servlets programmiert hat, weiss, wovon ich rede!
- Was ich deshalb suchte, war eine Möglichkeit, deklarativ erzeugtes HTML zu erzeugen. In ASP.NET habe ich bisher keine Möglichkeit gefunden, mit ASPX-Seiten Teile von HTML-Seiten zu erzeugen, d.h. ohne Header und Body-Tag. Dafür gibt es in JQuery die Methode load, der man als Argument nicht nur die URL einer ASPX-Seite mitgeben kann, sondern auch die ID des Tags, das daraus geladen werden soll. Damit holt sich JQuery das Snippet, das es für die Darstellung des Detailbereichs benötigt.
...load('IndexmSnippet.aspx #blumenDetailSource', ...)
Man beachte, dass es sich nicht um zwei Argumente handelt, sondern dass die ID mit Leerschlag getrennt direkt im gleichen Argument steht wie die aufzurufende Seite.
Unsere Überarbeitung besteht aus den folgenden Schritten
- Der Detailbereich wird in eine eigene Seite ausgelagert, wobei ein Parameter für den Datensatz mitgegeben wird
- In der Hauptseite erzeugen wir für jene Tags innerhalb des Repeaters, die wir im JQuery später ansprechen möchten, eindeutige IDs. Dabei verwenden wir als Nummerierung Container.ItemIndex, ein Befehl, der innerhalb eines Repeaters zur Verfügung steht.
- Schliesslich fehlen noch ein paar Zeilen JavaScript, um das Nachladen des Detailbereichs mit dem Ereignis expand auszulösen.
Als erstes kopiere ich die ursprüngliche Aspx-Seite und hänge der kopierten Seite das Suffix „Snippet“ an. Dann lösche ich aus dem Body das Suchfeld. Im ItemTemplate des Repeaters lösche ich jene Zeile mit dem <h3>-Tag. Den Detailbereich behalte ich natürlich, aber das <p>-Tag ersetze ich mit einem <span>, das die ID „blumenDetailSource“ erhält. Diese ID muss nicht eindeutig sein, weil nur der Inhalt des Tags in die Hauptseite geholt wird.
Das SQL in der DataSource-Komponente bleibt dasselbe, aber die Parameter ändern sich. Dann im Detailbereich stammt der gesuchte Blumenname nicht mehr aus dem Suchfeld, sondern aus einem Parameter name, der dem AJAX-Request mitgegeben wird.
<SelectParameters> <asp:QueryStringParameter DefaultValue="" Name="deutschername" QueryStringField="name" Type="String" /> <asp:QueryStringParameter DefaultValue="" Name="lateinischername" QueryStringField="name" Type="String" /> </SelectParameters>
Der Vorteil davon, dass wir in ASPX immer vollständige Seiten machen müssen, ist, dass man die Detailseite auch direkt testen kann, vorausgesetzt, man hängt dem Aufruf einen Parameter name= mit einem gültigen Namen an, z.B.
http://www.ecotronics.ch/mobile/IndexmSnippet.aspx?name=Anthemis tinctoria
Für den zweiten Schritt lösche ich in der Hauptseite alles, was innerhalb des Tags <p id=“blumenDetail“> steht. Das Tag selbst bleibt bestehen, denn in dieses Tag laden wir mit JQuery den von AJAX geholten Inhalt. Damit wir eine eindeutige ID für dieses Tag haben, ergänzen wir das Tag folgendermassen:
<p id='<%# "blumenDetail" + (Container.ItemIndex) %>'> </p>
Dies ist nicht zwingend notwendig, macht aber den Source-Code übersichtlicher. Denn wenn wir innerhalb eines Repeaters statische IDs vergeben, sind diese nicht mehr eindeutig. Das Framework ergänzt sie dann selbständig mit eindeutigen, aber in ihrer Länge ausgesprochen unübersichtlichen Benennungen.
Die gleiche Ergänzung machen wir auch beim <div>-Tag, das das einzelne Listenelement umgibt, und bei den zwei Label-Komponenten, mit denen wir den lateinischen oder deutschen Titel anzeigen. Mit den beschriebenen Modifikationen sieht der Repeater nun so aus:
<asp:Repeater ID="repKeimverhalten" runat="server" DataSourceID="dsKeimverhalten"> <ItemTemplate> <div id='<%# "myCollapsible" + (Container.ItemIndex) %>' data-role="collapsible" data-collapsed="true"> <h3> <asp:Label visible='<%#Request.Params.Get("l") == "l"%>' ID="blumenLateinischerName" name='<%# "blumenLateinischerName" + (Container.ItemIndex) %>' runat="server" Text='<%#Eval("lateinischername")%>'></asp:Label> <asp:Label ID="blumenName_d" name='<%# "blumenName_d" + (Container.ItemIndex) %>' runat="server" Text='<%#Eval("name_d")%>' visible='<%#Request.Params.Get("l") != "l"%>'></asp:Label> </h3> <p id='<%# "blumenDetail" + (Container.ItemIndex) %>'> </p> </div> </ItemTemplate> </asp:Repeater>
Dieser Code ist auch schon vorbereitet darauf, die Titelanzeige anhand der Buttons in der Fussleiste zwischen deutschen und lateinischen Namen zu wechseln.
Nun fehlt noch der dritte Schritt, das JavaScript:
<script type="text/javascript"> $(document).ready(function () { $("div[id^='myCollapsible']").bind('expand', function () { var blumenName = $(this).find("span[name^='blumen']").text(); $(this).find("p[id^='blumenDetail']").load('IndexmSnippet.aspx #blumenDetailSource', 'name=' + blumenName ); }); }); </script>
Für alle <div>-Tags, deren Id mit „Collapsible“ anfängt (das bedeutet id^=..), fangen wir das Event expand ab. Den Namen der Blume holen wir aus dem Span-Element, dessen Name mit „blumen“ beginnt. Diesen Namen übergebe ich als zweites Argument im AJAX-Aufruf load. Aufgerufen wird natürlich meine vorbereitete Snippet-Seite. Nun muss ich das HTML, das aus dem load-Aufruf zurückkommt, nur noch an das <p>-Tag, dessen Id mit blumenDetail beginnt, innerhalb des aufrufenden Collapsible-Tags hängen und fertig ist die Mobile-Liste.
Von hier aus sind weitere Optimierungen denkbar: Zur Zeit wird von Anfang an die volle Liste angezeigt, obwohl der User möglicherweise nur die ersten paar Titel überhaupt zu Gesicht bekommt. Eigentlich wäre es sinnvoll, die Liste erst nachzuladen, wenn der User nach unten scrollt. Aber das kommt vielleicht ein andermal.