Seitenvariablen automatisch im Viewstate speichern
ASP.NET verbessert das Web-Programmiermodell erheblich, indem es Inhalte von Formularfeldern nach einem Postback automatisch wieder einsetzt. Web Forms sind damit äußerlich genauso zustandsbehaftet wie Windows Dialoge. Werte von Variablen in Web-Forms-Klassen profitieren von diesem Fortschritt jedoch nicht. Sie verlieren bei jedem Postback ihren Zustand. Der Artikel zeigt, wie Sie dieses Defizit leicht beheben und das Web-Programmiermodell noch bequemer machen können.
|
Diesen Artikel können Sie dank freundlicher Unterstützung von ASP .NET professional auf MSDN Online lesen. |
Wenn Sie auch nur ein Web-Formular mit der alten ASP-Technologie programmiert haben, dann erinnern Sie sich daran, wie lästig es war, Formularfelder immer wieder von Hand zu füllen. Hier als Erinnerung an diese Zeiten die Beispielseite WebFormOld.asp; sie besteht aus einem Eingabefeld (Text1) und einer Submit-Schaltfläche:
<html> <body> <form action="WebFormOld.asp" method="post"> <INPUT name="Text1" type="text"> <INPUT type="submit" value="Abschicken"> </form> </body> </html>
Wenn Sie auf die Schaltfläche klicken, ruft das Formular dieselbe Seite noch einmal auf und schickt ihr dabei die Inhalte der Formulareingabefelder. Wenn Sie etwas in Text1 eingegeben haben, taucht diese Eingabe bei der erneuten Anzeige der Seite allerdings nicht (!) auf. Die Seite wird wieder 1:1 so erzeugt, wie sie definiert ist. Eine HTML- bzw. ASP-Seite kennt von Haus aus keinen Zustand (z.B. Steuerelementinhalte), der über Aufrufe hinweg erhalten bleibt.
Um den eingegebenen Text in Text1 beizubehalten, müssen Sie selbst Hand anlegen:
<INPUT name="Text1" type="text" value=<% =request("Text1") %>>
Sie müssen die Eingabe aus den beim Seitenaufruf zum Server gesandten Daten mit dem request-Objekt herauslesen und in den HTML-Code einbauen, welches die ASP-Seite generiert. Das ist umständlich, macht ASP-Seiten schnell unleserlich und entspricht nicht dem Programmiermodell von Windows Dialogen. Dort müssen Sie nämlich nichts tun, um nach einem Klick auf eine Schaltfläche und der Ausführung einer dahinter liegenden Ereignisbehandlungsroutine ihre Eingaben in Steuerelementen zu erhalten.
Die Web-Programmierung ist jedoch anders. Dort findet die Ereignisbehandlung auf dem Server statt und jedes Formular muss anschließend wieder komplett neu aufgebaut werden.
Daran ändert auch ASP.NET nichts. Aber ASP.NET verbirgt diese andere Funktionsweise im Vergleich zu WinForms-Anwendungen vor Ihnen für Steuerelemente. WebFormNew.aspx zeigt, wie einfach und transparent der Code für das obige Beispiel jetzt aussehen kann:
<html> <body> <form method="post" runat="server"> <INPUT type="text" name="Text1" runat="server"> <INPUT type="submit" value="Abschicken"> </form> </body> </html>
Sie müssen nicht mehr manuell eingreifen und programmieren, um die Steuerelementinhalte zu bekommen. Steuerelemente, die ihren Zustand automatisch beibehalten sollen, versehen Sie vielmehr nur mit dem Zusatz runat="server" (und auch das übernimmt VS.NET für Sie).
Server-Steuerelemente
Sie fragen sich jetzt vielleicht, wie ASP.NET dieses Kunststück vollbringt? Die Antwort lautet: Es verbirgt das alte ASP-Programmiermodell hinter einem serverseitigen Objektmodell. ASP.NET sieht Ihre Seite nicht als einen langen zu interpretierenden Text mit einem Gemisch von HTML und Scriptcode, sondern als Objektmodell. ASP.NET führt zur Laufzeit nicht Ihre .aspx-Seite aus, indem es sie interpretiert.
Kompilation statt Interpretation
Stattdessen übersetzt ASP.NET Ihre Seitendatei in IL-Code in einer DLL-Assembly (Listing 1). HTML ist also für ASP.NET eine Programmiersprache wie VB .NET. In dem übersetzten Code stehen nun aber keine response.write()-Anweisungen, sondern er baut bei jedem Seitenaufruf ein serverseitiges Objektmodell der Seite auf. Ausgangspunkt ist dafür die Methode __BuildControlTree() (Listing 1). Sie sehen darin zwar noch HTML-Code (z.B. in der Zeile IL_0004), aber der wird an Objekte zur Initialisierung übergeben (hier ein LiteralControl).
Für alle Steuerelemente, die ihren Zustand behalten sollen, generiert ASP.NET eine ähnliche Methode mit Namen __BuildControlXXX() (wobei "XXX" für die Steuerelement-ID steht).
Diese Methoden werden ausgehend von __BuildControlTree() geschachtelt aufgerufen und erzeugen einen Baum von Steuerelementobjekten. Listing 2 zeigt eine Skizze der Funktionsweise. Sie sehen den Call Stack, inklusive der wesentlichen Anweisungen innerhalb einer aufgerufenen Methode.
Listing 1: Auszug aus der Übersetzung von WebFormNew.aspx in IL-Code
.method private instance void __BuildControlTree(class [System.Web]System.Web.UI.Control __ctrl)
cil managed
{
...
.line 1:13
//000001: <html>
IL_0003: ldloc.0
IL_0004: ldstr "<html>\r\n\t<body>\r\n\t\t"
IL_0009: newobj instance void [System.Web]System.Web.UI.LiteralControl::.ctor(string)
IL_000e: callvirt instance void [System.Web]System.Web.UI.IParserAccessor::AddParsedSubObject
(object)
IL_0013: nop
.line 1:13
IL_0014: ldarg.0
IL_0015: callvirt instance class ...WebFormNew_aspx::__BuildControl__control2()
IL_001a: pop
.line 1:13
IL_001b: ldloc.0
IL_001c: ldarg.0
IL_001d: ldfld class ...WebFormNew_aspx::__control2
IL_0022: callvirt instance void [System.Web]System.Web.UI.IParserAccessor::AddParsedSubObject
(object)
IL_0027: nop
.line 1:13
IL_0028: ldloc.0
IL_0029: ldstr "\r\n\t</body>\r\n</html>\r\n"
IL_002e: newobj instance void [System.Web]System.Web.UI.LiteralControl::.ctor(string)
IL_0033: callvirt instance void [System.Web]System.Web.UI.IParserAccessor::AddParsedSubObject
(object)
IL_0038: nop
IL_0039: nop
IL_003a: ret
}
.method private instance class [System.Web]System.Web.UI.Control
__BuildControl__control2() cil managed
{
...
.line 3:13
//000003: <form method="post" runat="server">
IL_0001: newobj instance void [System.Web]System.Web.UI.HtmlControls.HtmlForm::.ctor()
IL_0006: stloc.1
IL_0007: ldarg.0
IL_0008: ldloc.1
IL_0009: stfld class ...WebFormNew_aspx::__control2
.line 3:13
IL_000e: ldloc.1
IL_000f: ldstr "post"
IL_0014: callvirt instance void [System.Web]System.Web.UI.HtmlControls.HtmlForm::set_Method
(string)
IL_0019: nop
IL_001a: ldloc.1
IL_001b: stloc.2
.line 3:13
IL_001c: ldloc.2
IL_001d: ldstr "\r\n\t\t\t"
IL_0022: newobj instance void [System.Web]System.Web.UI.LiteralControl::.ctor
(string)
IL_0027: callvirt instance void [System.Web]System.Web.UI.IParserAccessor::AddParsedSubObject
(object)
IL_002c: nop
.line 3:13
IL_002d: ldarg.0
IL_002e: callvirt instance class ...WebFormNew_aspx::__BuildControl__control3()
IL_0033: pop
.line 3:13
IL_0034: ldloc.2
IL_0035: ldarg.0
IL_0036: ldfld class ...WebFormNew_aspx::__control3
IL_003b: callvirt instance void [System.Web]System.Web.UI.IParserAccessor::AddParsedSubObject
(object)
IL_0040: nop
.line 3:13
IL_0041: ldloc.2
IL_0042: ldstr " \r\n\t\t\t<INPUT type=\"submit\" value=\"Abschicken\">\r\n\t\t"
IL_0047: newobj instance void [System.Web]System.Web.UI.LiteralControl::.ctor(string)
IL_004c: callvirt instance void [System.Web]System.Web.UI.IParserAccessor::AddParsedSubObject
(object)
IL_0051: nop
IL_0052: ldloc.1
IL_0053: stloc.0
IL_0054: br.s IL_0056
IL_0056: ldloc.0
IL_0057: ret
}
Listing 2: Skizze des Call Stack während des Aufbaus des WebFormNew.aspx-Objektmodells
__BuildControlTree(page)
page.AddParsedSubObject(new LiteralControl("<html><body>"))
__control2 = __BuildControl__control2()
__ctrl = new HtmlForm()
__ctrl.Method = "Post"
__control3 = __BuildControl_control3()
__ctrl = new HtmlInputText()
__ctrl.Attribute("type") = "text"
__ctrl.Name = "Text1"
return __ctrl
__ctrl.AddParsedSubObject(__control3)
__ctrl.AddParsedSubObject(new LiteralControl("<input type=submit ...>"))
return __ctrl
page.AddParsedSubObject(_control2)
page.AddParsedSubObject(new LiteralControl("</body></html>"))
Das Ergebnis der __BuildControl...()-Methoden ist am Ende ein Objektmodell der Form
Control LiteralControl HtmlForm HtmlInputText LiteralControl LiteralControl
HTML-Erzeugung
Wie Sie sehen, werden nur die HTML-Elemente, die mit runat="server" markiert sind, durch spezifische Html...-Objekte repräsentiert. Die grundsätzliche Funktionsweise der HTML-Erzeugung ändert sich dadurch aber nicht: Sobald die serverseitige Bearbeitung einer Seitenanfrage im Objektmodell abgeschlossen ist, wird die HTML-Erzeugung angestoßen. Ausgehend vom Wurzelobjekt erzeugen die Objekte HTML jeweils nur für sich selbst; sie rendern (engl. to render = dt. wiedergeben) sich in den Zeichenstrom, ähnlich wie sich graphische Objekte in eine Bitmap rendern. Oder anders ausgedrückt: Die Objekte serialisieren ihren Zustand nach HTML.
Der Objektzustand ist nun durch Zweierlei bestimmt:
-
durch das von Ihnen in der .aspx-Seite geschriebene HTML und
-
durch Zustandsänderungen während der serverseitigen Seitenaufrufbearbeitung.
Den ersten Punkt verdeutlicht ein Beispiel bei der Übersetzung der Formulardefinition:
Aus
<form method="post" runat="server">
wird im IL-Code
newobj instance void ...HtmlForm::.ctor() stloc.1 ... ldstr "post" callvirt instance void ...HtmlForm::set_Method(string)
also ähnlich wie
With new HtmlForm() .Method = "post" End With
Sie haben die Art, wie das Formular seine Daten an den Server sendet, im HTML-Code als Attribut des <form>-Elements angegeben. Serverseitig ist daraus eine Zuweisung an eine Eigenschaft eines Control-Objekts im Seitenobjektbaum geworden. Der durch Ihren HTML-Code definierte Zustand eines HTML-Servercontrols ist also statisch (auch wenn diese Objekte immer wieder bei jedem Seitenaufruf neu erzeugt werden).
Dynamisch können Sie den Zustand von Servercontrols allerdings in Ereignisbehandlungsroutinen ändern. Listing 3 zeigt ein simples Beispiel, in dem WebFormNew.aspx um ein Label-Steuerelement (txtLabel1) und einen Eventhandler für das Auslösen der Schaltfläche (btnSubmit1) erweitert wurde. (Unterschiede zwischen HTML-Steuerelementen (HtmlInputText für <INPUT>) und Web-Form-Steuerelementen (Label für <asp:Label>) sind für die Diskussion an dieser Stelle unwichtig.)
Listing 3: Verändern des Zustands eines Servercontrols in einer Ereignisbehandlungsroutine
WebFormNew.aspx: <%@ Page Language="vb" AutoEventWireup="false" Codebehind="WebFormNew.aspx.vb" Inherits="AutomaticViewstatePersistence.WebFormNew"%> <HTML> <body> <form method="post" runat="server"> <P> <INPUT id="txtText1" type="text" name="Text1" runat="server"> <INPUT type="submit" value="Abschicken" id="btnSubmit1" name="Submit1" runat="server"> </P> <P> <asp:Label id="lblLabel1" runat="server"></asp:Label> </P> </form> </body> </HTML>
WebFormNew.aspx.vb: Imports System.Web.UI.WebControls Imports System.Web.UI.HtmlControls Public Class WebFormNew Inherits System.Web.UI.Page Protected WithEvents lblLabel1 As Label Protected WithEvents btnSubmit1 As HtmlInputButton Protected WithEvents txtText1 As HtmlInputText #Region " Web Form Designer Generated Code " ... #End Region Private Sub btnSubmit1_ServerClick(...) Handles btnSubmit1.ServerClick lblLabel1.Text = txtText1.Value End Sub End Class
Welchen Zustand auch immer lblLabel1 in seiner __BuildControllblLabel1() Initialisierungsroutine zugewiesen bekam, der Eventhandler überschreibt einen Teil davon. Und wenn lblLabel1 später aufgefordert wird sich zu serialisieren, dann manifestiert sich diese Zustandsänderung in HTML.
Beim ersten Seitenaufruf stellt sich lblLabel1 folgendermaßen dar:
<span id="lblLabel1"></span>
Nach Eingabe von "hello, world!" in txtText1 mit anschließendem Klick auf die Submit-Schaltfläche und nach der Ausführung des zugehörigen Eventhandlers serialisiert sich lblLabel1 dann wie erwartet so:
<span id="lblLabel1">hello, world!</span>
Zustandserhaltung
So weit hört sich das alles plausibel und einfach an. Objektzustände werden nach HTML serialisiert. Und solange es dabei um Objektzustände geht, die sich aus Ihrem HTML-Code ergeben und somit im IL-Code fest verdrahtet werden können, ist das auch kein Problem.
Ein Problem sind aber z.B. Eingaben in ein <INPUT>-Element. Wie können sie über mehrere Seitenaufrufe erhalten bleiben? Sie stehen ja nicht im IL-Code und werden auch nicht in einer Ihrer Ereignisbehandlungsroutinen immer wieder zugewiesen. Müssten Sie das tun, wären Sie keinen Schritt vorangekommen gegenüber dem alten ASP-Programmiermodell.
Die Lösung ist jedoch einfach: Nach Aufbau des serverseitigen Objektmodells liest ASP.NET selbst dynamischen Zustand aus den Daten heraus, die dem Seitenaufruf vom Formular mitgegeben wurden. Irgendwann nach Aufruf von __BuildControlTree() und vor Ausführung Ihrer Eventhandler macht ASP.NET also folgendes:
txtText1.Text = request("Text1")
Indem ASP.NET diesen Arbeitsschritt automatisch und für Sie unsichtbar erledigt, hebt es Ihr Programmiermodell auf eine bequemere Ebene. "Hinter den Kulissen" hat es sich jedoch nicht wirklich verändert; HTTP ist weiterhin ein zustandsloses Protokoll.
Aber in Ihren Ereignisbehandlungsroutinen haben nun die Servercontrols den endgültigen Zustand, der sich aus dem ursprünglich fest verdrahteten im IL-Code und dem dynamischen in den empfangenen Formulardaten ergibt.
Es ist somit klar, dass HTML-Steuerelemente bei ASP.NET immer in Formularen stehen müssen. Alles andere macht keinen Sinn - zumindest nicht, wenn Sie deren Zustand automatisch erhalten möchten. Denn nur Eingaben in Steuerelemente in Formularen werden beim Auslösen des Formulars an den Server geschickt und stehen ASP.NET für die Zustandszuweisung an das Objektmodell zur Verfügung.
Viewstate
Dieses Vorgehen funktioniert wunderbar für den Zustand eines Steuerelements, der vom Client bei einem Seitenaufruf wieder zurück zum Server kommt. Das gilt aber z.B. nicht für den Zustand eines Label-Steuerelementes. Ein Label serialisiert sich - wie oben gezeigt - in ein -Element. Dessen Inhalt kehrt beim Auslösen eines Formulars aber nicht zurück zum Server; nur Werte von Steuerelementen tun das. Für alle anderen Zustandsinformationen auf beliebigen Servercontrols muss also ein anderer Weg gefunden werden, sie über Seitenaufrufe (oder genauer: Postbacks, das heißt eine Seite ruft sich selber auf) hinweg zu erhalten.
Zur Lösung dieses Problems hat Microsoft den sog. Viewstate eingeführt. Der Viewstate ist die Summe aller dynamischen Zustandsinformationen des serverseitigen Objektmodells, die zwar nach HTML serialisiert werden, aber bei einem Postback nicht zum Server zurückkommen. Dazu zählt der Inhalt eines Label-Controls genauso wie eine veränderte Breite eines Steuerelementes oder irgendein anderes Attribut eines Servercontrols, das ein HTML-Element repräsentiert.
Damit der Viewstate diese Aufgabe erfüllen kann und bei einem Postback wieder zum Server gelangt, speichert ASP.NET ihn nicht etwa in einer Session-Variablen ab - denn dort wäre er seitenunspezifisch -, sondern in einem verborgenen Steuerelement im Formular. Beim ersten Aufruf von WebFormNew.aspx aus Listing 3 sieht das erzeugte HTML daher wie folgt aus:
... <form method="post" action="WebFormNew.aspx" ...> <input type="hidden" name="__VIEWSTATE" value="dDwtODA4NjY3NTk7Oz4=" /> <input name="txtText1" id="txtText1" type="text" /> ... <span id="lblLabel1"></span> ...
Das value-Attribut des "__VIEWSTATE" <input>-Elements enthält alle Zustandsinformationen aus dem serverseitigen Objektmodell, von denen ASP.NET meint, sie müssten so aufbewahrt werden, um beim Auslösen des Formulars wieder zum Server zu gelangen.
Dass die serialisierten Objektzustände in einem Textformat in die HTML-Seite einzuschließen sind, ist klar. Eine Serialisierung nach XML, die sich anbieten würde, scheidet jedoch aus, weil das Ergebnis den Umfang der zwischen Browser und Server auszutauschenden Daten stark anschwellen ließe. Microsoft hat daher eine Serialisierung in ein Binärformat (bzw. sehr kompaktes Textformat) mit einer base64-Kodierung vorgezogen.
Wenn Sie also wissen wollen, welche Daten die Zeichenfolge "dDwt...4=" enthält, müssen Sie sie dekodieren. Die .NET Standardbibliothek bietet dafür eine Funktion:
Dim b() As Byte = Convert.FromBase64String("dDwt...4=")
Das Ergebnis ist dann eine ähnlich unverständliche Zeichenkette (alle Bytewerte entsprechen darstellbaren Zeichen):
t<-80866759;;>
Wenn Sie in das Textfeld des Formulars jetzt "hello, world!" eingeben und es abschicken, dann baut ASP.NET serverseitig das Objektmodell auf, liest dynamischen Zustand aus den Formularfeldern inklusive dem dekodierten deserialisierten Viewstate und führt Ihre Ereignisbehandlungsroutine aus. Die anschließend generierte Seite unterscheidet sich dann an drei Stellen von der vorherigen:
... <form method="post" action="WebFormNew.aspx" ...> <input type="hidden" name="__VIEWSTATE" value="dDwtODA4NjY3NTk7dDw7bDxpPDE+... 4=" /> <input name="txtText1" id="txtText1" type="text" value="hello, world!" /> ... <span id="lblLabel1">hello, world!</span> ...
Wie zu erwarten enthält das "Text1" <input>-Steuerelement wieder Ihre Eingabe (im value-Attribut) und der <span> für lblLabel1 umschließt den zugewiesenen Text.
Darüber hinaus hat sich aber auch der Viewstate verändert: Er ist jetzt sehr viel umfangreicher. Seinen Inhalt zeigt Abbildung 1. Welchen Aufbau er genau hat, ist unwichtig. Wichtig ist, wie Sie sehen, dass der dynamische Zustand von lblLabel1 in ihm festgehalten ist.
Abbildung 1: Der Viewstate enthält als Zustandsinformation für ein Label den ihm dynamisch zugewiesenen Text "hello, world!".
"hello, world!" ist damit in der erzeugten HTML-Seite dreimal vertreten: im Texteingabefeld, im vom Label erzeugten HTML und unsichtbar im Viewstate. Der Viewstate enthält den Text aber nur aus dem einzigen Grund, damit er bei einem Postback wieder zum Server gelangt. (Dafür reicht es übrigens nicht, dass die Zeichenkette auch im Texteingabefeld steht; denn für ASP.NET ist es ja nur ein Zufall, dass dessen Inhalt und der des Label-Controls gleich sind.)
Sie sehen, der Viewstate ist ein notwendiger Bestandteil der Daten, die zwischen Browser und Server in beiden Richtungen fließen, wenn Sie die Bequemlichkeit des ASP.NET Programmiermodells wollen. Er ist kein Ballast, auch wenn er sehr umfangreich werden kann, z.B. wenn Sie mit an Listensteuerelemente gebundenen DataSet-Objekten arbeiten. Der Viewstate ist ein Zugeständnis an die letztlich unverändert gebliebene und tiefer gelegene Schicht von Web-Anwendungen: HTTP.
Der blinde Fleck des Viewstate
So gut der Viewstate für serverseitige Steuerelemente funktioniert und so bequem er das Web-Programmiermodell für sie macht - er hat einen blinden Fleck: ASP.NET serialisiert in den Viewstate nur den Zustand von Steuerelementen, nicht aber von anderen globalen Variablen in Ihren Seitenklassen.
Überlegen Sie einmal, wie Sie folgendes Szenario lösen würden: Sie sollen ein Login-Formular programmieren, das dem Anwender nur eine bestimmte Anzahl von Versuchen gestattet, sich anzumelden.
Das klingt einfach und kann natürlich auch mit dem alten ASP gelöst werden - es ist aber mit den bisherigen Mitteln nicht wirklich "schön" zu realisieren. Die entscheidende Frage lautet nämlich: Wo halten Sie den Zähler, der die Login-Versuche protokolliert?
Er muss entweder serverseitig gespeichert werden oder mit jedem Seitenaufruf vom Client wieder mit zum Server kommen. Drei Lösungen bieten sich bisher an: die Session-Variable, ein verstecktes <input>-Steuerelement oder Cookie. Keine dieser Lösungen ist schön, sie alle "kämpfen" mit dem unglücklichen bisherigen Web-Programmiermodell.
Viel schöner wäre es, wenn Sie den Zähler einfach in einer Eigenschaft der ASP.NET-Seite halten könnten, wie beispielsweise mit _loginTrials in Listing 4 gezeigt (Layout der zugehörigen Seite siehe Abbildung 2). Der Authentifizierungscode ist absichtlich simpel gehalten wie auch das Ergebnis einer erfolgreichen Authentifizierung (hier: Weiterleitung an eine fest verdrahtete Seite). Entscheidend ist der Gebrauch der Zählervariablen.
Listing 4: Code für eine Login-Seite, die die Anmeldungsversuche in einer globalen Variable zählt und ab einer gewissen Anzahl weitere Versuche verweigert.
Public Class WebForm1
Inherits System.Web.UI.ViewstatePersistentPage
Protected WithEvents Label1 As System.Web.UI.WebControls.Label
Protected WithEvents Label2 As System.Web.UI.WebControls.Label
Protected WithEvents txtUser As System.Web.UI.WebControls.TextBox
Protected WithEvents Label3 As System.Web.UI.WebControls.Label
Protected WithEvents btnLogin As System.Web.UI.WebControls.Button
Protected WithEvents txtPassword As System.Web.UI.WebControls.TextBox
Protected WithEvents lblErrorMsg As System.Web.UI.WebControls.Label
Private Const MAX_LOGIN_TRIALS As Integer = 3
Protected _loginTrials As Integer
#Region " Web Form Designer Generated Code "
...
#End Region
Private Sub btnLogin_Click(...) Handles btnLogin.Click
_loginTrials += 1
If _loginTrials <= MAX_LOGIN_TRIALS Then
If txtUser.Text = "test" Then
If txtPassword.Text = "secret" Then
Server.Transfer("WebForm2.aspx")
Else
lblErrorMsg.Text = "Invalid password!"
End If
Else
lblErrorMsg.Text = "Unknown user!"
End If
Else
lblErrorMsg.Text = "Too many invalid login attempts!"
End If
End Sub
Public ReadOnly Property loggedInUser() As String
Get
Return txtUser.Text
End Get
End Property
Public ReadOnly Property loginTrials() As Integer
Get
Return _loginTrials
End Get
End Property
End Class
Abbildung 2: Layout einer simplen Login-Seite (Code Behind siehe Listing 4).
Leider kann der serverseitige Code so aber nicht korrekt funktionieren, weil die Zählervariable bei jedem Aufruf wieder den Wert 0 hat. Sie müssten sie selbst irgendwo auf einem der obigen Wege zwischenspeichern.
Transparente Viewstate-Persistenz
Warum aber einen Sonderweg beschreiten, wenn ASP.NET doch für andere Seiteneigenschaften wie txtUser selbst vormacht, dass es auch anders geht? Warum also nicht _loginTrials im Viewstate zwischenspeichern? Tatsächlich gibt es praktische Lösungen, die wir ganz transparent für Sie machen wollen.
Eigene Daten im Viewstate speichern
Den Inhalt der Zählervariablen im Viewstate zu speichern und später wieder daraus zu laden, ist denkbar einfach. Die ASP.NET Page-Klasse bietet dafür die Eigenschaft Viewstate. Hinter ihr steht ein StateBag-Objekt als Collection von Name-Wert-Paaren.
Public Class WebForm1
Inherits System.Web.UI.Page
...
Private Sub Page_Load(...) Handles MyBase.Load
_loginTrials = viewstate("zähler")
End Sub
Private Sub btnLogin_Click(...) Handles btnLogin.Click
_loginTrials += 1
viewstate.Add("zähler", _loginTrials)
If _loginTrials <= MAX_LOGIN_TRIALS Then
...
Sie würden damit den aktuellen Zählerstand zum frühestmöglichen Zeitpunkt aus dem Viewstate laden, das heißt direkt nach dem Seitenaufruf und vor sonstigen Ereignisbehandlungsroutinen. Und Sie würden ihn immer dann wieder setzen, wenn er sich verändert.
Doch genau dieses explizite Laden/Speichern ist so typisch für eine Web-Programmierung. Letztlich hätte sich nur der Aufbewahrungsort des Zählers gegenüber den obigen Lösungen geändert. Entscheidend für eine Viewstate-Speicherung von Seiteneigenschaften über die Steuerelemente hinaus ist aber, dass sie ebenfalls automatisch abläuft. Sie sollen nichts davon merken, wenn _loginTrials im Viewstate gehalten wird.
Halbautomatische Speicherung im Viewstate
Ein erster Schritt in Richtung wirklich transparenter Speicherung im Viewstate sind die virtuellen Methoden LoadViewState() und SaveViewState()der Page-Klasse. ASP.NET ruft sie für eine Seite auf, und zwar nach dem Aufbau des serverseitigen Objektmodells (und vor dem Load()-Event) bzw. nach dem Abschluss der Verarbeitung (das heißt nach Ihren Ereignisbehandlungsroutinen). Damit ließe sich die Login-Seite so formulieren:
Public Class WebForm1
Inherits System.Web.UI.Page
...
Protected Overrides Sub LoadViewState(ByVal savedState As Object)
MyBase.LoadViewState(savedState)
_loginTrials = viewstate("zähler")
End Sub
Protected Overrides Function SaveViewState() As Object
viewstate.Add("zähler", _loginTrials)
Return MyBase.SaveViewState()
End Function
Private Sub btnLogin_Click(...) Handles btnLogin.Click
_loginTrials += 1
If _loginTrials <= MAX_LOGIN_TRIALS Then
...
Sie beschränken den eigenen Umgang mit dem Viewstate auf zwei Routinen. Am Ort der Benutzung der Eigenschaft sieht man von ihrer Persistenz also nichts mehr. Dennoch ist diese Lösung höchstens halbautomatisch, weil Sie sie für jede Seite neu implementieren müssen. Denn die Information, welche Eigenschaften im Viewstate zu speichern bzw. daraus wieder zu laden sind, steht explizit in den beiden Methoden.
Automatisch wird die Speicherung erst, wenn Sie selbst das Laden/Speichern überhaupt nicht mehr codieren müssen, wenn ASP.NET einfach weiß, welche Eigenschaften im Viewstate zu speichern sind.
Vollautomatische Speicherung im Viewstate
Um eine vollautomatische Speicherung zu erreichen, müsste der Code in Load/SaveViewState selbstständig erkennen, welche Eigenschaften er speichern soll. Da können Sie es sich natürlich einfach machen, indem Sie immer alle Eigenschaften wählen oder alle Eigenschaften außer Steuerelementen oder alle Eigenschaften, die public sind.
Noch einfacher wäre es jedoch für den Code, wenn Sie die zu speichernden Eigenschaften kennzeichnen würden. Es wird meistens die Minderzahl der Seiteneigenschaften sein, so dass der Aufwand dafür nicht sehr hoch ist.
Solch eine Kennzeichnung könnte über eine Namenskonvention geschehen, zum Beispiel: "Der Name von Eigenschaften, die im Viewstate gespeichert werden sollen, endet immer auf ‚_persistent'.". Damit würde man aber den Zweck von Benennungen torpedieren, der sich an der Bedeutung einer Eigenschaft in der Geschäftslogik orientieren sollte und nicht an infrastrukturellen Anforderungen.
Viel besser ist es daher, die Metainformation über die Persistenz einer Eigenschaft auch als solche auszudrücken. Der .NET Framework bietet dafür als Mittel Attribute. Attribute sind typsichere, erweiterbare Metainformationen (siehe z.B. [Westphal]). Metainformationen geben Auskunft über andere Informationen. So sagt beispielsweise die Metainformation Public in
Public x As Integer
über den für x reservierten Speicherplatz aus, dass auf ihn von außen zugegriffen werden darf. Genauso sagt das Attribut Serializable in
<Serializable()> _ Public Class Person ... End Class
etwas über eine andere Information - hier eine Klasse - aus: Die Instanzen der Klasse können durch einen Formatierer serialisiert werden.
Das .NET Framework erlaubt Ihnen nun, selbst Attribute zu definieren, um eigene Aussagen über andere Informationen zu machen. Für das vorliegende Problem bietet der Sourcecode zu diesem Artikel ein selbst definiertes Attribut mit der Aussage, dass eine Eigenschaft automatisch im Viewstate gespeichert werden soll:
Public Class WebForm1 Inherits System.Web.UI.ViewstatePersistentPage ... <PersistToViewstate()> _ Protected _loginTrials As Integer Private Sub btnLogin_Click(...) Handles btnLogin.Click _loginTrials += 1 If _loginTrials <= MAX_LOGIN_TRIALS Then ...
Mehr müssen Sie nicht tun! Sie wünschen sich einfach nur ein Ergebnis, das Was Ihres Wunsches wird Ihnen erfüllt, ohne das Sie sich um das Wie kümmern müssen. Sie implementieren keine Zeile Code mehr, um die so gekennzeichneten Eigenschaften im Viewstate zu speichern bzw. daraus zu laden.
Voraussetzungen
Damit das Attribut seine Wirkung entfalten kann, müssen zwei Voraussetzungen erfüllt sein:
Erstens sollte der Typ des Attributs serialisierbar sein. Für einfache Typen (z.B. Integer, String) und deren Felder ist das von Haus aus gegeben. Für Ihre eigenen Typen müssen Sie diese Eigenschaft jedoch explizit, z.B. mit dem Serializable-Attribut, definieren:
<Serializable()> _ Public Class User ... End Class Public Class MyWebForm Inherits System.Web.UI.ViewstatePersistentPage ... <PersistToViewstate()> _ Protected _currentUser As User ...
Zweitens müssen Sie Ihre WebForm von der speziellen Seitenklasse ViewstatePersistentPage ableiten. Die letzten obigen Beispiele machen das vor. Diese Seitenklasse ist der Container für das reine Markierungsattribut PersistToViewstate (zum Begriff des Attributcontainers siehe [Westphal].)
Weitere Voraussetzungen für die automatische Viewstate-Persistenz gibt es nicht. Wenn Sie ein neues ASP.NET WebForm-Projekt in VS.NET anlegen, referenzieren Sie einfach die Komponente ViewstatePersistentPage.dll (siehe. Sourcecode zum Artikel) und ersetzen die Ableitung Ihrer WebForm-Klassen von Page durch ViewstatePersistentPage.
Funktionsweise
ViewstatePersistentPage ist das Mittel, um das Laden/Speichern der attributierten Eigenschaften für Sie wirklich transparent zu machen. Die Klasse implementiert die oben beschriebenen Load/SaveViewState-Methoden (Listing 5).
Listing 5: Die neue Basisklasse für Seiten nimmt Ihnen die Arbeit ab, Ihre persistenten Eigenschaften selbst im Viewstate zu verwalten.
<AttributeUsage(AttributeTargets.Field)> _ Public Class PersistToViewstateAttribute Inherits Attribute End Class Public Class ViewstatePersistentPage Inherits System.Web.UI.Page Protected Overrides Sub LoadViewState(ByVal savedState As Object) MyBase.LoadViewState(savedState) ProcessPersistentFields(False) End Sub Protected Overrides Function SaveViewState() As Object ProcessPersistentFields(True) Return MyBase.SaveViewState() End Function ...
Die entscheidende Funktionalität der Attributauswertung steckt aber in der Methode ProcessPersistentFields(). Sie ermittelt die Viewstate-persistenten Eigenschaften und lädt bzw. speichert sie (Listing 6).
Listing 6: Ermittlung der Eigenschaften einer Seite, die im Viewstate gespeichert werden sollen.
Private Const _VIEWSTATEKEY_PREFIX = "__PTV_" Private Sub ProcessPersistentFields(ByVal persist As Boolean) Dim t As Type = Me.GetType Do While Not t Is GetType(ViewstatePersistentPage) Dim fi As Reflection.FieldInfo For Each fi In t.GetFields(Reflection.BindingFlags.Instance Or _ Reflection.BindingFlags.Public Or _ Reflection.BindingFlags.NonPublic Or _ Reflection.BindingFlags.DeclaredOnly) If fi.IsDefined(GetType(PersistToViewstateAttribute), _ False) Then Dim viewstateKey As String = _VIEWSTATEKEY_PREFIX & fi.Name If persist Then viewstate.Add(viewstateKey, fi.GetValue(Me)) Else fi.SetValue(Me, viewstate(viewstateKey)) End If End If Next t = t.BaseType Loop End Sub
Im Grunde ist es sehr einfach, die Eigenschaften (oder genauer: die Felder) eines Objektes zu finden, die mit dem PersistToViewstate-Attribut gekennzeichnet sind. Sie benutzen dafür den Reflection API des .NET Frameworks in zwei Schritten:
Zunächst ermitteln Sie alle Felder des Seitenobjektes, indem Sie seinen Typ befragen; vereinfacht sieht das so aus:
Me.GetType().GetFields(...)
Sie bekommen ein Array von Reflection API FieldInfo-Objekten zurück. Jedes einzelne steht für eine Eigenschaft, im vorliegenden Fall also zum Beispiel label1, txtUser oder eben auch _loginTrials.
Dann befragen Sie diese FieldInfo-Objekte, ob auf ihnen das PersistToViewstate-Attribut definiert ist (für Details dazu siehe [Alvi, Roman, Westphal]).
Dim fi As FieldInfo For Each fi in Me.GetType().GetFields(...) If fi.IsDefined(GetType(PersistToViewstateAttribute), _ false) ... End If Next
Die Methode IsDefined() gibt Auskunft, ob mindestens ein PersistToViewstate-Attribut auf einer Eigenschaft definiert ist. Das reicht aus, da das Attribut lediglich ein Markierungsattribut ist, das heißt keine Funktionalität beinhaltet. Es dient nur als Flag, und der Container - konkret: die Methode ProcessPersistentFields() - weiß dann schon, was zu tun ist.
Wenn das Attribut an einer Eigenschaft hängt, lädt der Container dessen Wert aus dem Viewstate bzw. speichert ihn darin, je nach Modus, in dem er aufgerufen wurde. Laden und Speichern sind aber nicht direkt ausführbar, sondern müssen late bound durch den Reflection API erfolgen. Denn der allgemeine Code in der Methode weiß ja nichts über konkrete Eigenschaften der Seiten, die von seiner Klasse abgeleitet sind. Wertermittlung und -änderung müssen daher ebenfalls über das FieldInfo-Objekt laufen, dem dazu allerdings das Objekt mitzuteilen ist, um dessen Eigenschaftswert es geht:
Eigenschaftswert lesen: wert = fi.GetValue(Me). Eigenschaftswert verändert: fi.SetValue(Me, neuerWert).
Der Name des Viewstate Name-Wert-Paares für jede Eigenschaft ergibt sich dabei aus dem Eigenschaftennamen und einem Prefix.
Listing 6 ist allerdings nicht ganz so geradlinig wie die Beschreibung dieser Vorgehensweise. Der beschriebene Algorithmus steht in einer Schleife:
Dim t As Type = Me.GetType() Do While Not t Is GetType(ViewstatePersistentPage) ... t = t.BaseType End While
Der Grund dafür liegt im Vererbungsbaum Ihrer WebForm-Seiten. Für das obige Login-Beispiel sieht die Vererbungshierarchie des Seitenobjektes nämlich nicht (!) so aus:
WebForm1 ViewstatePersistentPage Page …
Vielmehr leitet ASP.NET für Ihre .aspx-Seite noch einmal eine Klasse von Ihrer Web-Form-Klasse ab. Die Vererbungshierarchie ist also in Wirklichkeit folgende:
WebForm1_aspx WebForm1 ViewstatePersistentPage Page …
Eigentlich sollte dieser kleine Unterschied keine Bedeutung für den Attributcontainer und den Algorithmus haben. WebForm1_aspx erbt ja alle Eigenschaften von WebForm1. Wenn ProcessPersistentFields() nach den Feldern von Me.GetType() fragt und Me vom Typ WebForm1_aspx ist, dann stehen auch die Felder der Basisklasse WebForm1 in der Liste. Allerdings sind das nur die Felder, die die abgeleitete Klasse auch sehen darf! Felder, die als Private gekennzeichnet sind, fehlen.
Nur um Ihnen keine Einschränkung in der Sichtbarkeit Ihrer persistenten Eigenschaften aufzuerlegen, wandert der Algorithmus den Vererbungsbaum in der Schleife hinab und fragt auf jeder Ebene nach zu speichernden Eigenschaften. Sobald er auf die Basisklasse ViewstatePersistentPage stößt, hört er jedoch auf, weil sie die Wurzel für Ihre Web-Form-Klassen darstellt.
Zusammenfassung
ASP.NET bietet ein sehr viel intuitiveres Programmiermodell als ASP. Sie sehen in Ihrem serverseitigen Code Seiten als Hierarchie von Steuerelementobjekten und können sicher sein, dass deren Zustand automatisch über Seitenaufrufe hinweg erhalten bleibt. Für andere Eigenschaften von Web-Form-Klassen greift diese Automatik aber nicht. Dadurch entsteht eine schmerzliche Lücke im Programmiermodell.
Die vorgestellte Lösung schließt diese Lücke: Indem Sie Ihre Web-Form-Klassen von ViewstatePersistentPage ableiten und Eigenschaften, deren Wert Sie über Seitenaufrufe hinweg erhalten möchten, mit dem PersistToViewstate-Attribut versehen, genießen Sie dieselbe Bequemlichkeit beim Umgang mit Ihren Objekten wie mit Steuerelementen.
So rückt die Programmierung von Web-Benutzeroberflächen wieder ein Stück näher an die von Windows-GUIs heran.
Ressourcen
[Alvi] S. Alvi, Attributes in C#, http://www.codeproject.com/csharp/attributes.asp
[Roman] S. Roman et al., Attributes, in: VB .NET Language in a Nutshell, O'Reilly 2002, ISBN 0-596-00308-0, http://www.ondotnet.com/pub/a/dotnet/excerpt/vbnut_8/index1.html
[Westphal] R. Westphal, Deklarative Programmierung durch erweiterbare Metadaten, in: OBJEKTspektrum 02/2003

