André Krämers Blog

Lösungen für Ihre Probleme

In meinem letzten Blog Eintrag habe ich erklärt, wie Ajax von Hand mit Hilfe des XMLHttpRequest Objekts implementiert werden kann.

Eine Alternative zu dieser händischen Implementierung ist die Nutzung des ASP.NET Client Callback Frameworks, welches mit der Version 2.0 von ASP.NET erschien.

Um Client Callbacks nutzen zu können muss eine ASP.NET Seite bzw. ein ASP.NET Control das Interface ICallbackEventHandler implementieren. Das Interface definiert die beiden Methoden:

RaiseCallbackEvent ist die Methode, den Aufruf des Clients entgegen nimmt. GetCallbackResult gibt das Ergebnis der serverseitigen Verarbeitung an den Client zurück.

Startet der Client eine Anfrage an eine Seite / ein Control welches ICallBackEventHandler implementiert, wird eine reduzierte Variante des ASP.NET Page Lifecycle / ASP.NET Control Lifecycle ausgeführt. Im wesentlichen entspricht die Callback Variante der “klassichen” Postback Variante, es fehlen jedoch die Methoden, die sich um das Rendering des Outputs kümmern. Außerdem werden keine Postback Events ausgeführt.

Das ganze sieht bezogen auf die Phasen also ungefähr so aus:

04lifecycle

Los gehts

Sehen wir uns das ganze nun einmal an einem konkreten Beispiel an. Als Ausgangsbasis soll eine modifizierte Kopie der Seite des letzten Beispiels mit folgendem Markup dienen:

<div>
    <p>
        <a href="#">Hier klicken zum Request einer statischen Datei </a>
        <br />
        <a href="#">Hier für Hello World WebService klicken </a>
        <br />
        <a href="#">Hierfür Echo WebService klicken. Geben Sie bitte vorher eine Zahl in nebenstehendem Feld ein: </a>&nbsp;
        <asp:TextBox ID="TextBox1" runat="server" Width="40px">4711</asp:TextBox>
    </p>
</div>
<div id="content">
    Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.
</div>

Der Server

Auf Serverseite müssen wir nun in unserer Seite das Interface ICallbackEventHandler implementieren. Ausgehend von unserem Markup gilt es drei Operationen zu unterstützen:

  • Zurückgeben des Inhalts einer statischen Datei
  • Aufruf einer parameterlosen Methode eines Webservices, die “Hello World” zurück gibt
  • Aufruf einer Methode Echo eines Webservices, welche einen numerischen Wert entgegen nimmt und einen String zurück gibt.

Welche Operation auszuführen ist, unterscheiden wir anhand des einzigen Arguments der Methode RaiseCallbackEvent: eventArgument.

public partial class Teil2 : Page, ICallbackEventHandler
  {
    private string callbackResult;
    public void RaiseCallbackEvent(string eventArgument)
    {
      switch (eventArgument)
      {
        case "static": this.ReadStaticFile();
        break;
        case "HelloWorld": this.CallHelloWorldService();
        break;
        case "Echo": this.CallEchoService();
        break;
        default: this.ReturnUnknownOperationError();
        break;
      }
    }
    public string GetCallbackResult()
    {
      return this.callbackResult;
    }

Je nach Wert des Arguments eventArgument wird eine andere Methode zur weiteren Verarbeitung aufgerufen. Jede dieser Methoden schreibt ihr Ergebnis wiederum in das Feld callbackResult. Dieses wird wiederum als Ergebnis der Methode GetCallbackResult an den Client zurück gegeben.

Der Inhalt der verarbeitenden Methoden sieht wie folgt aus:

Auslesen der statischen Datei:

private void ReadStaticFile()
{
  using (var reader = new StreamReader(Server.MapPath("~/static.html")))
  {
    this.callbackResult = reader.ReadToEnd();
  }
}

Aufruf der Methode HelloWorld des Webservices:

private void CallHelloWorldService()
{
  this.callbackResult = new AjaxDemoService().HelloWorld();

Aufruf der Methode Echo des Webservices:

private void CallEchoService()
{
  int value;
  int.TryParse(this.TextBox1.Text, out value);
  this.callbackResult = new AjaxDemoService().Echo(value);
}

Wie man beim Aufruf der Webservice Methoden sieht, geschieht dieser Aufruf nun vom Server aus und nicht vom Client. Dadurch dass im Gegensatz zum vorherigen Beispiel der Client nun nicht mehr direkt den Service ruft, musste ich mir einen Proxy auf Serverseite erstellen.

Schön, aber wie komme ich nun auf den Server?

Bisher haben wir zwar geklärt, was auf dem Server alles geschehen muss, damit Client Callbacks funktionieren, offen ist allerdings noch, wie man überhaupt vom Client auf den Server kommt.

Der Aufruf des Servers geschieht über die JavaScript Funktion WebForm_DoCallback. Einen entsprechenden Aufruf der Methode kann man sich über den ClientScriptManager der Seite anhand der Methode GetCallbackEventReference erzeugen lassen. Zeile 5 des folgenden Listing demonstriert dies.

protected void Page_Load(object sender, EventArgs e)
{
  var clientScript = Page.ClientScript;
  string callbackReference = clientScript.GetCallbackEventReference(this, "argument", "requestFinished", "context", true);
  string script = string.Format("function sendAjaxRequest(argument, context){{\n {0}; }}", callbackReference);
  if (!clientScript.IsClientScriptBlockRegistered("callback"))
    {
      clientScript.RegisterClientScriptBlock(this.GetType(), "callback", script, true);
    }
}

Die Methode GetCallbackEventReference hat in der höchsten Überladung folgende Parameter:

Der Einfachheit halber verpackt man den generierten Aufruf der Methode WebForm_DoCallback in eine eigene JavaScript Funktion, die nur noch die wirklichen variablen Parameter argument und context entgegen nimmt, wie in Zeile 7 geschehen. Anschließend registriert man wie in Zeile 9 - 11 noch einen ClientScriptBlock für diese Funktion, der wie folgt in die Seite gerendert wird.

  <script type="text/javascript">
  //<![CDATA[function sendAjaxRequest(argument, context) { WebForm_DoCallback('__Page', argument, requestFinished, context, null, true); } //]]>     
  </script>

Die generierte Methode sendAjaxRequest kann man anschließend wie folgt aufrufen:

<a href="#" onclick="sendAjaxRequest('static', showMessage());">Hier klicken zum Request einer statischen Datei </a>
<br />
<a href="#" onclick="sendAjaxRequest('HelloWorld', '');">Hier für Hello World WebService klicken </a>
<br />
<a href="#" onclick="sendAjaxRequest('Echo', null);">Hier für Echo WebService klicken. Geben Sie bitte vorher eine Zahl in nebenstehendem Feld ein: </a> 

Zeile 1 zeigt, wie der Parameter context genutzt wird. Ich übergebe einfach die JavaScript Funktion showMessage als Argument. Diese wird nun automatisch aufgerufen, ehe der Request gestartet wird. Dies ist zum Beispiel bei länger dauernden Ajax Aufrufen sinnvoll, bei denen man dem Benutzer einen Hinweis anzeigen möchte, wie folgendes Listing zeigt:

<script type="text/javascript">
  function showMessage()
  {
    var contentDiv = document.getElementById('content');
    contentDiv.innerHTML = 'Lade...';
  }
</script>

Wichtig ist, dass der Parameter keinen String entgegen nimmt (wie leider in vielen Beispielen fälschlicherweise gezeigt), sondern eine JavaScript Funktion!

Eigentlich Interessant für unser Beispiel ist jedoch nicht der Parameter context, sonder der Parameter argument. Der Inhalt dieses Parameters (und nur dieser!) wird an die Methode RaiseCallbackEvent des Servers übergeben. Er steuert somit die auszuführende Funktionalität.

Alles beim Alten?

Spielt man nun ein wenig mit der vorgestellten Lösung herum, bemerkt man schnell, dass der Aufruf der Methode Echo stets ausgibt, das man 4711 eingegeben hat - auch wenn der Wert der Textbox zwischenzeitlich geändert wurde.

Ursache hierfür ist, dass ASP.NET Client Callbacks für alle Formularfelder stets die initialen Werte zurücksendet.

Screenshot der Beispielanwendung

Abhilfe schafft folgender JavaScript Code:

  __theFormPostData = '';
  WebForm_InitCallback();

Diesen packt man einfach in eine JavaScript Funktion, z. B. mit dem Namen repostForm.

Gibt man diese nun als context während des Aufrufs an:

  <a href="#" onclick="sendAjaxRequest('Echo', repostForm());">Hier [...]

werden auch die geänderten Eingaben des Benutzers übertragen.

Screenshot zeigt dass die geänderten Werte übertragen wurden

Fazit

ASP.NET Client Callbacks sind eine schnelle und einfache Lösung um Ajax Funktionalitäten unter ASP.NET ab der Version 2.0 zu implementieren.

Bei aller Einfachheit sollte man jedoch beachten, dass diese Lösung auch einige Schwächen hat:

  1. Auf dem Server wird fast der komplette Page Lifecycle ausgeführt. Daher sollten Codebestandteile, die bei einem Callback nicht zwingend ausgeführt werden müssen immer durch ein if(!Page.IsCallback) … geklammert werden, um unnötige Wartezeiten zu verhindern.
  2. Bei jedem Callback wird der komplette Viewstate zum Server übertragen. Gerade bei großem Viewstate und schlechter Internetanbindung verlangsamt dieser Umstand die Ajax Anfrage enorm.
  3. Es kann immer nur ein Argument an den Server übergeben werden. Möchte man mehr als eins übergeben, muss man die Werte durch ein Trennzeichen trennen und auf der Serverseite auseinanderpflücken
  4. Das übergebene Argument wird nicht wieder vom Server auf den Client zurück übertragen. Somit ist es in der verarbeitenden Javascript Funktion auf dem Client unmöglich herauszufinden, welche Operation ursprünglich gestartet wurde. Abhilfe schafft auch hier ein zusammengesetzter Rückgabewert, oder aber eine eigene verarbeitende Funktion je Operation.

Hat man diese Schwächen jedoch im Blick, lassen sich mit ASP.NET Client Callbacks wunderbare Ajax Applikationen implementieren.

Unter der Haube arbeiten diese Client Callbacks übrigens genauso wie das händische Beispiel aus dem letzten Blogpost - nämlich mit dem XMLHttpRequest Objekt:

Screenshot des JavaScript Debuggers, zeigt Zugriff auf XMLHttpRequest Objekt

Es gibt 6 Kommentare

Comment by Martin Herber
Von Martin Herber | 29.10.2011 21:11
Hallo,erstmal herzlichen Dank für diesen interessanten Artikel.Kann man irgendwo Ihr Testprojekt runterladen?Mein Request wird auf dem Server sauber verarbeitet, aber irgendwie wird das callbackResult "nicht abgeholt"...MfG
Comment by http://hurby.myopenid.com/
Von | 29.10.2011 21:11
Hallo,erstmal herzlichen Dank für diesen interessanten Artikel.Kann man irgendwo Ihr Testprojekt runterladen?Mein Request wird auf dem Server sauber verarbeitet, aber irgendwie wird das callbackResult "nicht abgeholt"...MfG
Comment by http://hurby.myopenid.com/
Von | 29.10.2011 21:11
Hallo,erstmal herzlichen Dank für diesen interessanten Artikel.Kann man irgendwo Ihr Testprojekt runterladen?Mein Request wird auf dem Server sauber verarbeitet, aber irgendwie wird das callbackResult "nicht abgeholt"...MfG
Comment by André Krämer
Von | 29.10.2011 21:11
Vielen Dank für das Feedback!Das Beispielprojekt werde ich in den nächsten Tagen zum Download bereit stellen und in diesem Blogeintrag verlinken.
Comment by Alexander H&amp;#246;lzle
Von Alexander H&amp;#246;lzle | 29.10.2011 21:11
Hallo,möcht mich auch erst mal für die interresante Serie bedanken, aber ich hatte das selbe Problem wie Martin weil ich nicht wusste wo ich die Daten, die mir der Server liefert, auslesen konnte.habs jetzt einfach mal so gemacht.function requestFinished() { var contentDiv = document.getElementById('content'); contentDiv.innerHTML = arguments[0]; }Funktionieren tut´s weis nur nicht ob es auch so gedacht war.Grüße Alex
Comment by Andreas
Von Andreas | 12.10.2014 17:01
Hallo,
vielen Dank für das gut erklärte Tutorial. In diesen Beispiel kann ich Codesnippets nicht einzelnen Dateien zuordnen. Mal ist auch von html Dateien die Rede mal ist es offensichtlich aspx Code. Es fällt mir schwer das Beispiel nachzubauen. Ist das Beispiel zu den Blogbeiträgen mittlerweile verfügbar?

Gruß

Andreas