André Krämers Blog

Lösungen für Ihre Probleme

Zu Anfang hoch gelobt, später ausgepfiffen und verpönt. Was sich im ersten Moment nach der typischen Geschichte deutscher Castingshow Teilnehmer anhört, ist in Wirklichkeit das Schicksal des ASP.NET AJAX Updatepanels.

Woll kaum ein anderes Control hat nach seiner Einführung einen solchen Hype verursacht und ist später so tief gefallen.

Der Reihe nach

Ehe wir im dritten Teil meiner ASP.NET Ajax Serie prüfen werden, ob das Updatepanel seinen schlechten Ruf zu recht oder unrecht hat, möchte ich zunächst jedoch kurz ein paar allgemeine Worte über das Steuerelement verlieren.

Das Updatepanel kam mit den ASP.NET Ajax Extensions in der Version 1.0 für das .NET Framework 2.0 auf den Markt und musste für Visual Studio 2005 noch separat heruntergeladen und installiert werden. Ab Visual Studio 2008 ist das UpdatePanel sofort von Hause aus in der Version 3.5 des ASP.NET Ajax Frameworks enthalten.

Ziel des Updatepanels ist es, ASP.NET Entwicklern die Möglichkeit zu geben Ajax zu implementieren, ohne selbst clientseitigen Code schreiben zu müssen. Im Gegensatz zu den Client Callbacks, die ich im Teil 2 der Serie vorgestellt habe, können ASP.NET Entwickler also in Ihrer Komfortzone - dem Server - bleiben.

Der klassische Ansatz über ASP.NET Postbacks

Basis für den heutigen Blogpost wird wieder eine leicht modifizierte Variante des Beispielcodes der vorherigen beiden Teile der Serie sein. Da das Updatepanel aus Entwicklersicht rein serverseitig arbeitet, sind zunächst einige Veränderungen am bestehenden Quellcode notwendig.

Zum einen sind sämtliche HTML Links (a Tags) durch ASP.NET LinkButtons zu ersetzen. Außerdem habe ich den HTML Div Tag, der das Ergebnis der Click-Aktionen anzeigte, durch ein ASP.NET Literal Control ersetzt. Den zuvor in die Seite integrierten JavaScript Code des letzten Beispiels habe ich komplett entfernt.

  <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Teil3.aspx.cs" Inherits="Teil2" %>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
      <title></title>
    <body>
      <form id="form1" runat="server">
      <h1>
        ASP.NET Webforms Anwendungen und Ajax (Teil3): Das Updatepanel
      </h1>
    <div>
      <p>
      <asp:LinkButton ID="StaticLinkButton" runat="server" OnClick="StaticLinkButton_Click">Hier klicken zum Request einer statischen Datei</asp:LinkButton>
    <br />
      <asp:LinkButton ID="HelloWorldLinkButton" runat="server" OnClick="HelloWorldLinkButton_Click">Hier für Hello World WebService klicken</asp:LinkButton>
    <br />
      <asp:LinkButton ID="EchoLinkButton" runat="server" OnClick="EchoLinkButton_Click">Hier für Echo WebService klicken. Geben Sie bitte vorher eine Zahl in nebenstehendem Feld ein:</asp:LinkButton>
    &nbsp;<asp:TextBox ID="EchoTextBox" runat="server" Width="40px">4711</asp:TextBox>
      </p>
    </div>
      <asp:Literal ID="contentLiteral" runat="server">Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.</asp:Literal>
      </form>
    </body>
  </html>

Serverseitig hat der Code keine Überraschungen parat. Je HTML Link Button gibt es einen Handler für das Click Event, in dem der Inhalt des Literal Controls gesetzt wird.


  protected void StaticLinkButton_Click(object sender, EventArgs e)
    {
      this.contentLiteral.Text = this.ReadStaticFile();
    }
  protected void HelloWorldLinkButton_Click(object sender, EventArgs e)
      {
       this.contentLiteral.Text = this.CallHelloWorldService();
      }
  protected void EchoLinkButton_Click(object sender, EventArgs e)
        {
         this.contentLiteral.Text = this.CallEchoService();
        }
  private string ReadStaticFile()
          {
            string fileContent;
            using (var reader = new StreamReader(Server.MapPath("~/static.html")))
              {
                fileContent = reader.ReadToEnd();
              }
            return fileContent;
          }
  private string CallHelloWorldService()
          {
            return new AjaxDemoService().HelloWorld();
          }
              private string CallEchoService()
              {
                int value;
                int.TryParse(this.EchoTextBox.Text, out value);
                return new AjaxDemoService().Echo(value);
              }

Startet man die Seite nun, sieht man dass ein GET Request auf die Seite Teil3.aspx abgesetzt wird. Die Antwort umfasst 2.1 kb.

01ersterRequest

Klickt man nun auf einen der Link Buttons, wird ein Postback ausgeführt und die komlette Seite neu vom Server auf den Client übertragen. Dies überrascht auch nicht, schließlich wurde noch keinerlei Ajax Funktionalität implementiert.

02normalerPostback

Ein erster Versuch mit dem Updatepanel

Um die Seite nun zu “ajaxifizieren”, benötigen wir zwei Controls. Das erste ist der ASP.NET Scriptmanager. Seine Hauptaufgabe ist es, Scripte zum Client zu liefern. Er muss vor allen AJAX Controls innerhalb der Seite erscheinen. Außerdem ist noch anzumerken, dass der Scriptmanager nach dem Highlander prinzip arbeitet: “Es kann nur einen geben!” - nun ja, zumindest pro Seite. Andernfalls würde zur Laufzeit eine InvalidOperationException ausgelöst werden.

Dies bedeutet übrigens, dass ich beim Einsatz von Masterpages keinen ScriptManager auf einer Contentseite platzieren darf, falls  auf der Masterpage bereits ein Scriptmanager genutzt wurde. Stattdessen müsste ich auf der Contentseite einen ScriptmanagerProxy einsetzen. Diesen benötige ich allerdings nur, falls ich auf der Contentseite deklarativ noch weitere Scripte hinzufügen möchte, die auf der Masterpage nicht gebraucht werden.

Da ich langsam etwas ausschweifend werde: Zurück zum Thema!

Das zweite Control, das wir auf unserer Seite brauchen ist ein Updatepanel. Es dient als Container des Bereichs, der später aktualisiert werden soll. Updatepanels dürfen im Gegensatz zum Scriptmanager übrigens mehrfach auf einer Seite vorkommen.

 03toolbox

Nachdem ich Scriptmanager und Updatepanel auf die Seite gezogen, und die die Clientcontrols in das Updatepanel verschoben habe, sieht das Markup meiner Seite wie folgt aus.


    <body>
      <form id="form1" runat="server">
      <asp:ScriptManager ID="ScriptManager1" runat="server"> </asp:ScriptManager>
      <h1>
        ASP.NET Webforms Anwendungen und Ajax (Teil3): Das Updatepanel
      </h1>
      <asp:UpdatePanel ID="UpdatePanel1" runat="server">
      <ContentTemplate>
        <p>
          <asp:LinkButton ID="StaticLinkButton" runat="server" OnClick="StaticLinkButton_Click">Hier klicken zum Request einer statischen Datei</asp:LinkButton>
        <br />
          <asp:LinkButton ID="HelloWorldLinkButton" runat="server" OnClick="HelloWorldLinkButton_Click">Hier für Hello World WebService klicken</asp:LinkButton>
        <br />
          <asp:LinkButton ID="EchoLinkButton" runat="server" OnClick="EchoLinkButton_Click">Hier für Echo WebService klicken. Geben Sie bitte vorher eine Zahl in nebenstehendem Feld ein:</asp:LinkButton>
          <asp:TextBox ID="EchoTextBox" runat="server" Width="40px">4711</asp:TextBox>
        </p>
        <asp:Literal ID="contentLiteral" runat="server">Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.</asp:Literal>
      </ContentTemplate>
      </asp:UpdatePanel>
      </form>
    </body>

Da keine Anpassungen im Code Behind meiner Seite notwendig sind, kann ich die Anwendung nun direkt starten.

Ein Blick auf den Netzwerkverkehr zeigt, dass bei der ersten Anfrage der Seite neben der eigentlichen Seite noch drei JavaScript Dateien übertragen wurden. Die Verweise auf diese drei Script Dateien wurden durch den ScriptManager automatisch in die Seite eingetragen. Allerdings werden nun statt der initialen 2.1 kb knapp 90 kb zum Client übertragen.

04ersterRequestMitUpdatePanel

Den eigentlichen Vorteil sehen wir, sobald wir auf einen der Links klicken. Nicht nur, dass die Seite nicht komplett neu geladen wird und somit das lästige flackern beim Seitenneuaufbau entfällt, auch die Menge der übertragenen Daten hat sich auf 1,5 kb reduziert.

05AsyncRequest1Post

Anscheinend haben wir also alles richtig gemacht. Leider aber auch nur anscheinend. Ein näherer Blick auf die Antwort des Servers zeigt, dass neben dem aktualisierten Inhalt auch das Markup für unsere drei Aktionslinks sowie die Textbox erneut übertragen wurden.

06AsyncRequest1Antwort

Das liegt daran, dass wir einen klassichen Updatepanel Anfängerfehler gemacht haben. Wir haben den kompletten Inhalt der Seite in das Updatepanel geschoben. Somit wird dieser auch vollkommen korrekt wieder vollständig vom Server zum Client übertragen.

Das ist selbstverständlich nicht das, was wir wollten. Schließlich ist unsere Ajax Implementierung nur augenwischerei wenn im Hintergrund doch wieder (fast) alles zum Client übertragen wird.

Ein zweiter Anlauf

Was können wir also tun? Nun, da wir nur den Inhalt des Steuerelements contentLiteral aktualisieren möchten, sollten wir offensichtlich auch nur diesen in das Updatepanel aufnehmen. Dazu schieben wir einfach die drei Aktionslinks wieder über das Updatepanel.

Wenn wir die Seite nun starten, stellen wir jedoch fest, dass ein Klick auf einen der Aktionslinks wieder einen vollständigen Postback inklusive komplettem Rendern der Seite auslöst.

Die Ursache hierfür ist, dass ein Updatepanel standardmäßig nur einen partiellen Postback verursacht und sich somit erneuert, wenn eines seiner Childcontrols einen Postback initiert.

Glücklicherweise lässt sich dieses Verhalten jedoch leicht ändern. Für jedes Updatepanel können sogenannte Trigger registriert werden, die einen partiellen Postback auslösen. Ein Trigger ist dabei nichts anderes, als ein Event eines Servercontrols.

Das Registrieren eines solchen Triggers kann entweder über den Designer oder über das ASP.NET Markup geschehen. Um es über das Markup zu lösen, müssen wir unser Beispiel wie folgt anpassen:


    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
    <ContentTemplate>
      <asp:Literal ID="contentLiteral" runat="server">Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.</asp:Literal>
    </ContentTemplate>
      <Triggers>
        <asp:AsyncPostBackTrigger ControlID="StaticLinkButton" EventName="Click" />
        <asp:AsyncPostBackTrigger ControlID="HelloWorldLinkButton" EventName="Click" />
        <asp:AsyncPostBackTrigger ControlID="EchoLinkButton" EventName="Click" />
      </Triggers>
    </asp:UpdatePanel>

Lädt man die Seite nun erneut, stellt man fest, dass die Aktionslinks wieder partielle Postbacks auslösen. Außerdem werden auch nur die notwendigen Daten übertragen.

07AsyncRequest2Antwort

Zu viel des Guten

Das Ergebnis sieht eigentlich schon ganz gut, so das man meinen könnte, dass wir nun fertig wären. Leider gibt es aber noch ein kleines Problem. Um dies zu verdeutlichen lege ich ein weiteres Updatepanel auf der Seite an. Dieses Panel erhält ein Literal-Control, welches serverseitig mit der aktuellen Uhrzeit gefüllt wird.


    <asp:UpdatePanel ID="UpdatePanel2" runat="server">
      <ContentTemplate>
        <asp:Literal ID="TimeLiteral" runat="server"></asp:Literal>
      </ContentTemplate>
    </asp:UpdatePanel>

  protected void Page_Load(object sender, EventArgs e)
  {
    this.TimeLiteral.Text = DateTime.Now.ToLongTimeString();
  }

Meine Erwartungshaltung wäre, dass bei einem Klick auf einen der drei Aktionslinks nur das ursprüngliche Updatepanel aktualisiert wird, da die Links nur hier als Trigger registriert wurden. Das neue Updatepanel sollte nicht aktualisiert werden.

Ein kurzer Test zeigt dass dem nicht so ist:

08_2UpdatePanels108_2UpdatePanels2

Wie die beiden Abbildungen zeigen, wurde nicht nur der Bereich des ersten Updatepanels, sondern auch der des zweiten Updatepanels aktualisiert.

Glücklicherweise lässt sich dieses Verhalten jedoch steuern! Das Updatepanel verfügt über eine Eigenschaft UpdateMode. Diese gibt an, wann sich das Updatepanel aktualisieren soll. Standardmässig ist diese Eigenschaft mit dem Wert Always belegt. Stellt man den Wert jedoch auf Contidional, wird das Updatepanel nur noch aktualisiert, wenn entweder eines seiner Childcontrols einen Postback initiert hat, einer der registrierten Trigger ausgelöst wurde, oder explizit die Methode Update serverseitig gerufen wurde.

Die folgenden Abbildungen zeigen die Auswirkung der Änderung des UpdateModes:

08_2UpdatePanels3

08_2UpdatePanels4

08_2UpdatePanels5

Fazit

Richtig eingesetzt ist das Updatepanel meiner Meinung nach besser als sein Ruf und für einfache Szenarien durchaus geeignet. Ajax Funktionalitäten können mit wenig Aufwand und ohne manuellen Scriptcode implementiert werden.

Achtet man darauf, nicht die komplette Seite in ein Updatepanel zu integrieren und den UpdateMode auf Conditional zu setzen, kann das Updatepanel in vielen Szenarien guten Gewissens genutzt werden. Man sollte sich allerdings darüber im Klaren sein, dass serverseitig bei jeder Anfrage der komplette Page-Lifecycle durchlaufen wird. Zeitaufwändige Operationen im Page_Load können - ähnlich wie bei den Client Callbacks - die Ajax Anfragen demnach erheblich verlangsamen!

Was man machen kann, wenn man den Pagelifecycle nicht komplett durchlaufen möchte, werde ich übrigens im nächsten Teil dieser Serie zeigen.