André Krämers Blog

Lösungen für Ihre Probleme

Nachdem ich in Teil 1 ASP.NET Webforms Anwendungen und Ajax (Teil 1) der Serie den manuellen Ansatz zur AJAX Implementierung gezeigt habe und in den Teilen 2 ASP.NET Webforms Anwendungen und Ajax (Teil 2) Client Callbacks und 3 “ASP.NET Webforms Anwendungen und Ajax (Teil 3) Das Updatepanel auf (halb-)automatische Alternativen einging, möchte ich dieses Mal einen anderen Weg zeigen, ASP.NET Scriptservices in Verbindung mit ASP.NET AJAX.

Rekapitulieren wir noch einmal:

Teil 1 ASP.NET Webforms Anwendungen und Ajax (Teil 1) der Serie zeigte einen manuellen Ansatz, der hauptsächlich damit kämpfte, dass man innerhalb des JavaScript Codes selbst darauf achten musste, die verschiedenen Besonderheiten der unterschiedlichen Browser zu berücksichtigen. Dies geschah in den Teilen 2 ASP.NET Webforms Anwendungen und Ajax (Teil 2) Client Callbacks und 3 ASP.NET Webforms Anwendungen und Ajax (Teil 3) Das Updatepanel zwar automatisch hinter den Kulissen, dafür hatten wir bei diesen Alternativen jedoch das Problem, dass bei jedem AJAX Request sowohl der komplette ViewState übertragen wurde und vor allem auf dem Server auch der komplette Page Life Cycle durchlebt wurde.

Leichtgewichtig durch die Ajax Welt

Heute möchte ich deshalb einen Ansatz zeigen, der die Vorteile der bisher vorgestellten Ansätze vereint, jedoch nicht über die selben Nachteile verfügt.

Konkret bedeutet dies, dass die Implementierung auf Clientseite browserübergreifend stattfinden wird, zwischen Client und Server nur die wirklich notwendigen Daten übertragen werden und der Page Life Cycle auf der Serverseite nicht durchlaufen wird.

Page Methods

Der erste Schritt Richtung einer leichtgewichtigen Implementierung führt über die sogenannten Page Methods. Page Methods sind statische Methoden einer *.aspx Seite, die mit dem [WebMethod] Attribut annotiert werden. Für den Client erscheinen Page Methods wie normale Methoden eines Web Services.

Ausgehend vom Beispielcode der letzten drei Teile der Serie passe ich den Servercode für die Methode, die den Inhalt einer Datei zurückliefert also wie folgt an:

  using System.IO;
using System.Web.UI;
using System.Web.Services;

public partial class Teil4 : Page
{
    [WebMethod]
    public static string ReadStaticFile()
    {

         string fileContent;
         using (var reader =
             new StreamReader(
                 System.Web.HttpContext.Current.Server.MapPath("~/static.html")))
         {

             fileContent = reader.ReadToEnd();
         }
         return fileContent;
     }
}

Als einzige Neuerung im Vergleich zur vorherigen Version fällt das zuvor beschriebene Attribut [WebMethod] auf.

Um die Methode ReadStaticFile nun clientseitig über das ASP.NET Ajax Framwork nutzen zu können, müssen wir innerhalb der *.aspx Seite bei unserem ScriptManager noch die Eigenschaft EnablePageMethods mit dem Wert true versehen.

<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="True"/>

Als Ergebnis rendert der Server bei einem Aufruf der Seite nun folgenden Java Script Code inline in die Seite hinein:

 <script type="text/javascript">
         var PageMethods = function() {

            PageMethods.initializeBase(this);
            this._timeout = 0;
            this._userContext = null;
            this._succeeded = null;
            this._failed = null;
         }
         PageMethods.prototype = {
             _get_path: function() {
                 var p = this.get_path();
                 if (p) return p;
                 else return PageMethods._staticInstance.get_path();
             },
             ReadStaticFile: function(succeededCallback, failedCallback, userContext) {
                 /// <param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
                 /// <param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
                 /// <param name="userContext" optional="true" mayBeNull="true"></param>
                 return this._invoke(this._get_path(), 'ReadStaticFile', false, {}, succeededCallback, failedCallback, userContext);
             }
         }
         // und vieles mehr ;-)

Wie man sieht, wird ein JavaScript Objekt PageMethods angelegt, welches über eine Funktion ReadStaticFile verfügt. Die Funktion hat also genau denselben Namen wie unsere serverseitige Funktion. Weiter nimmt Sie drei optionale Parameter entgegen:

  • succeededCallBack: Funktion, die aufgerufen wird, wenn die Ajax Anfrage erfolgreich beendet wurde
  • failedCallBack: Funktion, die aufgerufen wird, wenn die Ajax Anfrage fehlerhaft beendet wurde
  • userContext: Der Inhalt dieser Variable wird an die Funktionen succeededCallBack bzw. faildCallBack durchgereicht.

Der clientseitige Aufruf der Page Method gestaltet sich nun relativ einfach. Als erstes fügen wir der *.aspx Seite einen neuen ClientScriptBlock hinzu:

<script type="text/javascript">
  function getStaticFile() {
    PageMethods.ReadStaticFile(onSuccess, onError, "MeinKontext");
  }

  function onSuccess(result, userContext, methodName) {
    var contentDiv = $get('content');
    if (userContext) {
      result += "<p>Der UserContext war: <strong>" + userContext + "</strong></p>";
    }

    result += "<p>Der Methoden Name war: <strong>" + methodName + "</strong></p>";
    contentDiv.innerHTML = result;
  }

  function onError(result) {
    var contentDiv = $get('content');
    contentDiv.innerHTML = result;
  }

</script>

Einstiegspunkt für unseren späteren Code (Click Ereignis unseres Links) ist die Funktion getStaticFile. Sie ruft die Methode ReadStaticFile des Objekts PageMethods. Als Argumente übergeben wir zum einen die Funktionen für die erfolgreiche bzw. fehlerhafte Behandlung der Ajax Anfrage sowie als drittes die Zeichenfolge “MeinKontext”.

Die Funktion onSuccess ist nun die Funktion, die die (erfolgreiche) Antwort des Servers verarbeitet. Sie nimmt neben der eigentlichen Antwort des Servers, auch den zuvor übergebenen userContext entgegen. Außerdem wird der Name der aufgerufenen Servermethode im Parameter methodName übergeben. Da JavaScript relativ flexibel ist, was die Anzahl der Parameter angeht und Funktionen einzig durch ihren Namen und nicht anhand ihrer Signatur identifiziert, können die beiden letzten Parameter übrigens auch problemlos weg gelassen werden.

Innerhalb der Methode onSuccess holen wir uns via $get zunächst eine Referenz auf das Div Element in das wir später unser Ergebnis schreiben möchten. $get ist übrigens eine Funktion des ASP.NET Ajax Client Frameworks. Sie ist (vereinfacht gesagt) die Kurzschreibweise für die JavaScript Funktion document.getElementById.

Weiter werden noch userContext (falls vorhanden) und der aufgerufene Methodenname an das Ergebnis angehangen. Im letzten Schritt wird das komplette Ergebnis schließlich in unser Container Div geschrieben.

Der Aufruf der Funktion getStaticFile geschieht schließlich im Event onclick eines HTML Links (a Tag).

<p>
  <a href=# onclick=getStaticFile()>Hier klicken zum Request einer statischen Datei
  </a>
// ....
</p >
<div id=content>
  Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.
</div>

Schweres Leichtgewicht

Ruft man die Seite nun im Browser auf, fällt auf, dass neben der eigentlichen Seite auch drei JavaScript Dateien des Microsoft ASP.NET Ajax Client Frameworks geladen werden.

Erster Request der Seite

Die erste Anfrage umfasst nun also 93 kb, von denen sich 85 kb auf die MS AJAX Script Dateien verteilen.

Seinen Vorteil spielt die Herangehensweise jedoch bei den Ajax Anfragen aus. Wie man im folgenden Screenshot sieht, gehen hier gerade einmal 127 Byte über die Leitung. Weiter erkannt man deutlich, dass während des Posts kein ViewState mehr zum Server übertragen wird.

02_ajaxpost

Auch die Antwort ist relativ schmal und beschränkt sich auf das nötigste:

03_ajaxantwort

Die Antwort ist übrigens im JSON (Java Script Object Notation) Format serialisiert. Hätte unser Post Daten in Form von Argumenten für unsere Page Method enthalten, wären diese auch automatisch im JSON Format serialisiert worden. Eine etwas besser lesbare Variante der JSON formatierten Antwort enthalt der Tab JSON des Firebugs:

04_ajaxjson

In unserem Beispiel ist die Antwort zwar noch recht übersichtlich, so dass wir den Tab JSON eigentlich nicht benötigen würden, spätestens bei der Rückgabe komplexer Objektstrukturen lernt an den Tab jedoch schnell zu schätzen ;-)

Es geht noch besser!

Obwohl die bisherige Implementierung bereits ganz gut ist, gibt es noch zwei Kritikpunkte. Zum einen erscheint die Plazierung einer statischen Methode innerhalb der *.aspx Seite ein wenig eigenartig. Da sie weder auf Instanz Exemplar-Felder, noch auf Exemplar-Methoden der Seite zugreifen kann, ist sie innerhalb der Seite eigentlich fehl am Platz.

Außerdem stört mich der automatisch generierte JavaScript Script Block innerhalb der Seite, der die Proxies für die PageMethod Aufrufe zur Verfügung stellt. Lieber wäre mir die Variante einer eingelinkten externen JavaScript Datei.

Beide Punkte können wir glücklicherweise recht einfach lösen, nämlich durch den Einsatz von Script Services. Script Services sind normale Web Services, die mit dem ScriptService Attribut annotiert werden. Erstellt man einen Web Service mit Visual Studio 2008, ist dieses Attribut sogar bereits über der Klasse enthalten. Allerdings wird es automatisch auskommentiert.

Um einen Web Service nun also als Script Service zur Verfügung zu stellen, genügt es den Kommentar, wie in Zeile 13 des folgenden Listings gezeigt, zu entfernen.

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
[System.Web.Script.Services.ScriptService]
public class AjaxDemoService : WebService
{
    [WebMethod]
     public string HelloWorld()
     {
         return "Hello World";
     }

     [WebMethod]
      public string Echo(int number)
      {
          return string.Format("Sie haben {0} eingegeben.", number);
      }
}

Prinzipiell reicht dies aus, um den Service via Ajax anzusprechen. Um uns das Leben jedoch ein wenig einfacher zu machen, lassen wir das ASP.NET Ajax Framework für uns - analog zur Page Method Lösung - JavaScript Proxies für unsere Methoden Aufrufe erstellen. Dazu fügen wir innerhalb des ScriptManager Tags der Seite ein Kind Element <Services> ein. Diesem Element fügen wir als weiteres Kind Element eine ServiceReference ein, deren Pfad auf unseren Service verweißt.

<asp:ScriptManager ID=ScriptManager1 runat=server EnablePageMethods=True>
  <Services>
    <asp:ServiceReference Path=~/AjaxDemoService.asmx />
  </Services>
</asp:ScriptManager>

An dieser Stelle sei noch einmal darauf hingewiesen, dass dieser Schritt reine Bequemlichkeit und nicht zwingend erforderlich ist. Ohne ihn könnten wir unseren Service genauso via JavaScript rufen und würden genauso JSON als Serialisierungsformat nutzen können. Allerdings müssten wir den clientseitigen Aufruf dann komplett selbst entwickeln und hätten keine Proxies zur Verfügung.

Da wir jedoch bequem sind, lassen wir uns Proxymethoden generieren. Diese können wir dann in unserem bestehenden Client Script Block innerhalb der Seite aufrufen.

Wie die folgende Abbildung zeigt, erhalten wir dann sogar Intellisense und erfahren somit, welche Argumente eine bestimmte Methode entgegen nimmt.

05_VS

Der Rest gestaltet sich ähnlich der Page Method Implementierung. Als wichtiger Unterschied wäre noch zu erwähnen, dass die generierten Proxies standardmäßig in eine externe JavaScript Datei geschrieben werden und somit nicht mehr als Client Script Block in der Seite auftauchen.

Die Kommunikation zwischen Client und Server läuft analog der Page Method Implementierung.

Der Client sendet in seiner Anfrage unter anderem einen Header Content-Type mit dem Wert “application/json; charset=utf-8”. Dieser ist besonders wichtig, denn er steuert den Serialisierungsmechanismus des Servers. Hat dieser Header einen anderen Wert, wird die Antwort XML Serialisiert. Somit ist sichergestellt, dass JavaScript Clients JSON erhalten, wohingegen andere Clients XML erhalten.

06_ajaxheader2

Die nächste Abbildung zeigt, dass auch die Post Parameter via JSON serialisiert werden:

07_ajaxpost2

Für die Antwort ist dies natürlich auch weiterhin der Fall:

07_ajaxantwort2

Fazit:

Dieser Teil der Serie hat gezeigt, dass sich die Menge der übertragenen Daten während eines Ajax Requests mit der Hilfe von Page Methods, Script Services und dem ASP.NET Ajax Framework auf ein Minimum reduzieren lassen. Außerdem wird bei dieser Variante der ASP.NET Page Life Cycle im Gegensatz zu den vorherigen Beispielen nicht durchlaufen.

Beides in Kombination kann zu gewaltigen Performancevorteilen verglichen mit Client Callbacks oder dem Updatepanel führen.

Einziger Wermutstropfen ist die Übertragung der 85 kb großen ASP.NET Ajax Framework Java Script Dateien während des ersten Requests.

Eine Abhilfe für dieses Problem werden wir uns im nächsten Teil der Serie ansehen.

Wie immer freue ich mich natürlich über Kommentare zu diesem Blog Beitrag.