[C#] ViewState

Wat doet ViewState:

  1. Slaat waarden op per control, zoals een hashtable.
  2. Volgt veranderingen van een ViewState's geinitialiseerde status
  3. Serialiseert en deserialiseert opgeslagen data in verborgen form fields op de cliënt
  4. Zet automatisch de ViewState waarden waarden terug bij postbacks.
1. De ViewState slaat waarden op per control
ViewState slaat waarden op zoals een hashtable dat ook doet. De ViewState heeft een index die bereikbaar is via een key van het type string. De index accepteert elk object als waarde:

ViewState["Key1"] = 123.45am; // slaat een datum op
ViewState["Key2"] = 132;// slaat een integer op
ViewState["Key3"] = "hello world!"; // slaat een string op

Eigenlijk is ViewState gewoon maar een naam. ViewState is een protected property die is gedefinieerd in de System.Web.UI.Control class. Hiervan erven alle controls zoals server controls, user controls en pages. Het type van de property is System.Web.UI.StateBag. Strict gesproken heeft de StateBag class niks te maken ASP.NET. Het is nu eenmaal gedefinieerd in de System.Web assembly, maar naast de afhankelijkheid van de State Formatter (ook gedefinieerd in de System.Web assembly) is er geen reden waarom de StateBag class niet in dezelfde namespace als de ArrayList zit, de System.Collections namespace. In praktijk gebruiken de Server Controls de ViewState als hun opslagplaats voor de meeste, zoniet alle, properties. Dit geldt voor bijna alle Microsofts ingebouwded controls (label, textbox, button). Dit is belangrijk. Je moet dit begrijpen over de controls die je gebruikt. Server Controls gebruiken de ViewState als hun opslagplaats voor de meeste, zo niet alle, properties. Een traditionele (C#) property ziet er als volgt uit:
public string Text
{
get {return _text; }
set {_text = value; }
}

Wat belangrijk is om te weten is dat niet alle properties van ASP.NET er zo uitzien. In plaats hiervan wordt gebruik gemaakt van de ViewState StateBag opslagplaats:
public string Text
{
get { return (string)ViewState["Text"]; }
set { ViewState["Text"] = value; }
}

Dit geldt dus voor bijna alle PROPERTIES, zelfs STYLES (feitelijk doen Styles het door IStateManager te implementeren, maar ze doen het op dezelfde manier). Bij het schrijven van eigen controls is het een goed idee om dit patroon aan te houden. Het is ook balangrijk om te begrijpen hoe wordt omgegaan met standaard waarden. Als je aan een property denkt met een standaard waarde, dan zou dit op de volgende traditionele manier worden gedaan:
public class MyClass {

private string _text = "Default Value!";

public string Text {
get { return _text; }
set { _text = value; }
}
}
De standaard waarde is de waarde die wordt geretourneerd als de property nooit geset is. ViewState gaat als volgt met standaard waarden om:
public string Text {

get { return ViewState["Text"] == null ? "Default Value!" : (string)ViewState["Text"]; }
set { ViewState["Text"] = value; }
}
Net zoals een hashtable retourneert de StateBag null als de key geen waarde heeft of als de key niet bestaat. Dus als de waarde null is, is het niet geset en dan retourneert hij de standaard waarde. Anders retouneert hij datgeen dat geset is. In het geval dat de ViewState property op null wordt gezet zorgt dit er voor dat de property terug wordt gezet op de standaard waarde. Als een 'standaard' property op null wordt gezet dan bevat deze de waarde null. Hij resulteert dan niet de standaardwaarde (tenzij zo geimplementeerd natuurlijk). Dit is maar een reden waarom ASP.NET de voorkeur geeft om String.Empty("") te gebruiken in plaats van null. Het is ook niet balangrijk voor de ingebouwde controls omdat van nature alle properties die null kunnen zijn, die zijn standaard al null. Houd het dus in gedachten bij het schrijven van eigen controls.

2. De ViewState houdt veranderingen bij
De StateBag is niet zomaar een domme collectie van keys en values zoals een hastable. Een StateBag heeft namelijk een 'Tracking' mogelijkheid. Tracking staat aan of uit en als het eenmaal is aangezet kan het niet meer worden uitgezet. Tracking wordt aangezet door TrackViewState() aan te roepen. Wanneer tracking aanstaat wordt elke waarde die veranderd wordt in de StateBag gemarkeerd als 'dirty'. StateBag heeft een methode die gebruikt kan worden om te kijken of een waarde 'dirty' is, genaamd IsItemDirty(string key). het is ook mogelijk om handmatig een waarde als 'dirty' te markeren: SetItemDirty(string key). Om dit te illustreren het volgende voorbeeld. Hierbij is de StateBag niet aan het tracken.
stateBag.IsItemDirty("key"); // returns false
stateBag["key"] = "abc";
stateBag.IsItemDirty("key"); // still returns false

stateBag["key"] = "def";
stateBag.IsItemDirty("key"); // STILL returns false

stateBag.TrackViewState();
stateBag.IsItemDirty("key"); // yup still returns false

stateBag["key"] = "ghi";
stateBag.IsItemDirty("key"); // TRUE!

stateBag.SetItemDirty("key", false);
stateBag.IsItemDirty("key"); // FALSE!
tracking maakt het mogelijk om de veranderingen in de StateBag te volgen sinds het moment dat TrackViewState() is aangeroepen. Waarden die voor de aanroep zijn geset worden buiten beschouwing gelaten. Het is belangrijk om te weten dat elke waarde die wordt toegekent aan een item als 'dirty' wordt gemarkeerd, zelfs als dit dezelfde waarde is als die er al in stond:
stateBag["key"] = "abc";
stateBag.IsItemDirty("key"); // returns false
stateBag.TrackViewState();
stateBag["key"] = "abc";
stateBag.IsItemDirty("key"); // returns true
ViewState zou geschreven kunnen zijn om oude en nieuwe waarden te vergelijken om te bepalen of een item 'dirty' moet zijn of niet. Maar het opnieuw aanroepen van die ViewState geeft elk object de mogelijk om die waarde te zijn, dus je praat niet echt over eenvoudige string vergelijkingen. Daarnaast hoeft het object niet IComparable geiimplementeerd te hebben dus je hebt het ook niet over een simpele CompareTo. Omdat serialisatie en deserialisatie plaatsvindt wordt een instantie die in de ViewState wordt gezet niet dezelfde instantie na een postback. Dit soort vergelijkingen is niet belangrijk voor de ViewState om zijn werk te doen, dus doet hij dat ook niet. Tot zover tracking in een notendop.

Je zou je kunnen afvragen waarom StateBag uberhaupt deze mogelijkheid nodig heeft. Waarom zou iemand willen weten of er veranderingen zijn opgetreden sinds TrackViewState is aangeroepen? Waarom zouden zij niet gewoon gebruik maken van een collectie met items? Dit punt schijnt grote verwarring te scheppen als het gaat over de ViewState. Om goed te begrijpen waarom Tracking nodig is, is het nodig om te begrijpen hoe ASP.NET declaratieve controls opzet. Declaratieve controls zijn controls die zijn gedefinieerd in het ASPX of ASCX formulier:
< id="lbl1" runat="server" text="Hello World">
Het volgende punt wat duidelijk moet worden is de mogelijkheid van ASp.NET om gedeclareerde attributen te verbinden met control properties. Wanneer ASp.NET het formulier parsed en het de tag runat="server" vind, creeert het een instantie van het betreffende control. De naam van de variabele die de instantie krijgt is gebaseerd op het ID van de control (velen realiseren zich niet dat het niet verplicht is om een ID op te geven, ASP.NET gebruikt een automatisch gegenereerde ID als het ID niet gespecifiseerd is). Maar er gebeurt meer dan dit. De control's tag kan meerdere attributen hebben. In het label hierboven is er een attribuut "Text" met als waarde "Hello World". Door reflectie kan ASP.NET detecteren of er een property bestaat bij die naam. Indien een property (Text) bestaat met die naam wordt de property geset met de gedclareerde waarde (Helle World). Het attribuut is gedeclareerd als een string, dus als de property geen string accepteert zorgt ASP.NET ervoor dat de waarde naar het juiste type wordt geconverteerd.

Nog even ter herhaling: Server Controls maken gebruik van de ViewState om hun data op te slaan. Dit betekent dat wanneer er een attribuut wordt gedeclareerd op een server control, de waarde uiteindelijk wordt opgeslagen als een entry in dat control's ViewState StateBag. En nu even terug hoe tracking ook alweer werkt. Als de StateBag gezet is op tracking en er een waarde wordt geset dan wordt dit item gemarkeerd als 'dirty'. Als het niet aan het tracken is dan wordt het niet gemarkeerd. De vraag is dus, wanneer ASP.NET de SET aanroep van een PROPERTY welke correspondeert met het ATTRIBUUT die is GEDECLAREERD op het control, is dan de StateBag aan het tracken of niet? Het antwoord is nee, hij is niet aan het tracken. Dit omdat tracking niet begint totdat ieamand TrackViewState() aanroept op de StateBag. ASP.NET doet dit tijdens de OnInit fase van de page/control levenscycles. De kleine truck die ASP.NET gebruikt om properties te vullen staat het toe om gemakkelijk verschillen te detecteren tussen een declarateive set waarde en een dynamische set waarde. Als je niet realiseert waarom dit belangrijk is, blijf dan lezen.

3. Serialisatie en deserialisatie
Afgezien van het feit how ASP.NET declaratieve controls creert, de eerste twee mogelijkheden van de ViewState die zijn besproken tot nu toe waren strict verbonden met de StateBag class (hoe deze lijkt op de hashtable en hoe tracking werkt). Nu wordt besproken hoe ASP.NET de ViewState StateBag's gebruikt om de magie te laten gebeuren

hashtable, and how it tracks dirty values). Here is where things get bigger. Now we will have to start talking about how ASP.NET uses the ViewState StateBag's features to make the (black) magic of ViewState happen.

Als je de bron bekijkt van een ASp.NET pagina zie je de serialisatie van de ViewState. De ViewState is opgeslagen in een verborgen field genaamd __ViewState met een base64 geëncodeerde string.

Voordat wordt besproken hoe ASP.NET tot deze geëncodeerde string komt, is het noodzakelijk om de hierarchie van de controls op de pagina te weten. Vele ontwikkelaars realiseren zich niet dat een pagina bestaat uit een boom van controls omdat zij allemaal werken met ASPX pagina's en zij hoeven zich alleen zorgen te maken over de controls die direct gedeclareerd zijn op de pagina's. Maar controls kunnen op hun beurt ook weer controls bevatten (child controls) en een child controls kan ook weer controls onder zich hebben. Op deze manier ontstaat de boom van controls. De ASPX pagina zelf is de root(wortel) van deze boom. Het tweede niveau zijn alle controls die direct onder de root liggen. Gebruikelijk zijn dit maar drie controls 1) een literal control die de inhoud representeert vóór de form tag, 2) een htmlform die de form representeert met al zijn child controls en 3) een laatste literal control die alle content bevat ná de form tag. Het derde niveau zijn alle controls die in de controls van niveau twee vallen. Elk control in de boom structuur heeft zijn eigen ViewState, zijn eigen instantie van de StateBag class. Er is een protected method gedefinieerd in de System.Web.UI.ontrol class genaamd SaveViewState welke een object retourneert. Door deze methode recursief aan te roepen op elk control in de boomstructuur, bouwt ASp.NET een andere tree op wiens structuur hetzelfde is als de control boom structuur op, alleen bevat deze boom geen controls maar data.

Op dit punt is de data nog niet geconverteerd naar de string die zichtbaar is in het hidden field, het is gewoon een object boom waarin data is opgeslagen. Dit is het punt waarop alles samen komt. Op het moment dat de StateBag wordt gevraagd om zijn status op te slaan en te retourneren (StateBag.SaveViewState()) doet hij dat enkel voor de items die gemarkeerd zijn als 'dirty'. Om deze reden heeft de StateBag de tracking mogelijkheid. Dit is ook de enige reden waarom hij deze heeft. StateBag kan elk item dat er in is opgeslagen verwerken, maar waarom moet data die niet is veranderd van zijn standaard declaratieve status bewaard blijven? Er is geen reden voor om dat niet te doen. Het wordt teruggezet bij de volgende aanvraag wanneer ASP.NET de pagina opnieuw parsed (eigenlijk wordt het maar één keer geparsed, het bouwt een gecompileerde class die het werk van dan af aan doet). Ondanks deze slimme optimilizatie van ASP.NET, onnodige data blijft bewaard in de ViewState ondanks er misbruik van wordt gemaakt. Later volgen er voorbeelden over de misbruiken.

Stel dat je twee bijna identieke .ASPX pagina's heb met beide één label erop:


< id="form1" runat="server">
< id="label1" runat="server" text="">
< /form>


Het enige verschil tussen deze twee pagina's is de waarde van de Text property. In de eerste pagina is de waarde gezet op "abc" en in de tweede pagina staat het eerste couplet van het Wilhemus:

"Wilhelmus van Nassouwe
ben ik, van Duitsen bloed,
den vaderland getrouwe
blijf ik tot in den dood.
Een Prinse van Oranje
ben ik, vrij onverveerd,
den Koning van Hispanje
heb ik altijd geëerd"

Als van beide pagina's de bron wordt bekeken in de browser zul je zien dat in beide gevallen het hidden field __ViewState even groot is. Ook al zou je een button op de twee pagina's zetten welke enkel voor een postback zorgt, dan nog blijft de waarde in __ViewState hetzelfde. Dit komt omdat de Text attribuut van de label de gedeclareerde waarde krijgt voordat de ViewState wordt getracked. Dit betekent dat als je de 'dirty' flag zou checken van de Text item in de StateBag, de waarde niet als 'dirty' staat gemarkeerd. De StateBag negeert items die niet dirty zijn wanneer SaveViewState() wordt aangeroepen. Het object dat deze methode retourneert wordt geserialiseerd en in __ViewState geplaatst. En omdat het Text attribuut niet als 'dirty' staat gemarkeerd, wordt deze dus ook niet geserialiseerd en dus ook niet in het hidden __VieState field geplaatst.

Het deseriliseren wordt in ASP.NET afgehandeld door LosFormatter (v1.x) of de ObjectStateFormatter (v2.0).

4 Automatisch de data terugzetten
De ViewState deserialiseert de ViewState data en DAN pas vult hij de controls met data. De LoadViewState() methode in System.Web.Ui.Control (nogmaals, dit is de class dat elk control, user control en page erft) heeft parameter van het type object. Dit is het tegenovergestelde van de SaveViewState() methode die eerder is besproken, welke een object retourneert. Net zoals SaveViewState(), stuurt LoadViewState() de aanroep door naar zijn StateBag object. De StateBag vult dan opnieuw zijn key / object collectie met de data in het object. Belangrijk om te weten is dat de data die wordt teruggegeven via LoadViewState() enkel items zijn die als 'dirty' gemarkeerd zijn van de vorige request. Voordat de items van de ViewState geladen worden, kan de StateBag al waarden in zich hebben. Deze waarden kunnen afkomstig zijn van de declarateive properties, maar dit kunnen ook waarden zijn die door de ontwikkelaar gezet zijn voordat LoadViewState() is aangeroepen. Als een item meegestuurd wordt in LoadViewState() en dit item bestaat al, dan wordt deze overschreven.

Als een pagina voor wordt geladen tijden een postback (zelfs voor initialisatie) worden alle properties gezet naar hun gedeclareerde waarde. Dan wordt het OnOnit event afgevuurd. Tijdens deze fase roept ASP.NET TrackViewState() aan op alle StateBags. Daarna wordt LoadViewState() aangeroepen met de gedeserialiseerde data welke 'dirty' was van de vorige request. De StateBag roept Add(key, value) for elk van deze items. Omdat de StateBag op dit punt aan het tracken is, markeert het de waarde als 'dirty' zodat de waarde bewaart blijft voor de volgende postback.

Onjuist gebruik van ViewState
Nu de werking van ViewState een beetje duidelijk is volgen er nu een aantal punten die snel foutgaan bij ontwikkelaars. De volgende punten worden besproken:

1. Een standaard waarde forceren op een User Conrol
2. Statische data bewaren
3. Goedkope data bewaren
4. Dynamisch child control initialiseren
5. Dynamisch gemaakt controls initaliseren

1. Een standaard waarde forceren op een User Control
In deze situatie wil de ontwikkelaar een standaard waarde teruggeven aan een property als de waarde niet gezet is. Hiervoor zou de ontwikkelaar de volgende code kunnen hebben geschreven:
public class JoesControl : WebControl {

public string Text {
get { return this.ViewState["Text"] as string; }
set { this.ViewState["Text"] = value; }
}


protected override void OnLoad(EventArgs args) {

if(!this.IsPostback) {

this.Text = Session["SomeSessionKey"] as string;
}

base.OnLoad(e);
}
}
Bovenstaande code is niet correct. Er zijn namelijk twee problemen met deze aanpak. De eerste heeft te maken met het feit dat de Text property nooit gezet kan worden. Dit komt omdat in de Load event de standaard waarde wordt geset en daardoor de Text property gemarkeerd wordt als 'dirty'. De juiste code ziet er als volgt uit:
public class JoesControl : WebControl {

public string Text {
get {
return this.ViewState["Text"] == null ?

Session["SomeSessionKey"] :
this.ViewState["Text"] as string;
}

set { this.ViewState["Text"] = value; }
}
}
Een bijkomendheid is dat de code minder is geworden. Het OnLoad verhaal is weggelaten. De StateBag retourneert null als de sleutel niet bestaat.

2. Statische data bewaren
Met statische data wordt in dit geval data bedoeld die niet veranderd tijdens het bladeren door een website. Hierbij kan bijvoorbeeld gedacht worden aan een gebruiker die is inglogd. Om bijvoorbeeld aan te geven dat de gebruiker is ingelogd kan je dit als volgt aangeven:
(Shoppingcart.aspx)


(Shoppingcart.aspx.cs)
protected override void OnLoad(EventArgs args) {
this.lblUserName.Text = CurrentUser.Name;
base.OnLoad(e);
}
Met deze code is niks mis, het werkt prima. Het enige punt is alleen dat van het label nu ViewState tracking aanstaat. Dit komt omdat er geen gedclareerde data is gezet (de Text property is leeg) en in de code de Text property wordt gewijzigd. De text property wordt 'durty' en dus getracked en dus komt dit in de __ViewState te staan. Het zijn maar een paar bytes die per pagina aanroep worden geset, maar toch kan het efficienter. Dit kan op twee manieren. De eerste manier is om de ViewState uit te schakelen op het label control:
En dat is het, probleem opgelost, maar er is een betere manier. Het label control is namelijk één van de te veel gebruikte controls. Labels worden namelijk gerenderd als een span tag. Je moet je dus eigenlijk afvragen of je een span element nodig heb, zo niet dan gebruik je het literal control. Je gebruikt het literal control als je geen style wilt toepassen aan de tekst. In dit geval zou je dus gewoon
<%= CurrentUser.Name %>
kunnen gebuiken. Maar omdat je scheiding wilt tussen de UI en inline code en je het literal control wil toepassen is dit de tweede oplossing:
Er is geen onnodige span aangemaakt en de ViewState wordt niet bijgehouden.

3. Goedkope data bewaren
Goedkope data lijkt op statische data en is ook statische data, maar goedkope date is niet altijd statische data. Soms is er data die veranderd tijdens de levensduur van een applicatie, mogelijk van moment tot moment, maar deze data is virtueel vrij te verkrijgen. Met vrij wordt bedoeld dat de performance om het op te halen is minimaal. Een veelvoorkomend vergissing in dit geval is bijvoorbeeld het vullen van een dropdown lijst met de staten van de V.S.. De lijst met staten gaat binnekort niet veranderen... De dropdown list wordt gevuld met waarden uit de database.
protected override void OnLoad(EventArgs args) {

if(!this.IsPostback) {

this.lstStates.DataSource = QueryDatabase();
this.lstStates.DataBind();
}

base.OnLoad(e);
}
Van nature maken in ASP.NET databound controls gebruik van de ViewState om de lijst met items te onthouden. Op dit moment zijn er in totaal 50 staten in de V.S.. De dropdown list bevat niet alleen een ListItem voor elke staat, maar elke staat en de bijbehorende staat code worden geserialiseerd en geecodeerd in de ViewState. Dat is veel data die wordt overgedragen elke keer als een pagina wordt geladen, zeker over een inbelverbinding.

Net zoals het probleem met statische data, de algemene oplossing voor dit probleem is om gewoonweg de ViewState uit te schakelen op het control. Helaas gaat dit niet altijd werken. Het hangt of van het type control dat de data bind en van welke mogelijkheden je afhankelijk bent. Als in dit voorbeeld EnabeleViewState="false" wordt gezet en de if(!IsPostBack) wordt weggehaald, dan zou de ViewState niet meer worden geladen met de 50 staten, maar er vindt meteen een nieuw plaats. De dropdown zet niet langer de geselecteerde item terug na een postback. De reden hiervoor dat de geselecteerde item na een postbak niet wordt onthouden komt niet door EnableViewState="false". Postback controls, zoals een dropdown list en textbox zetten ook hun geposte date terug als de ViewState is uitgeschakeld. Ondanks de ViewState uitgeschakeld, is het control nog steeds in staat om de waarde te posten. Het vergeet de geselecteerde waarde omdat de binding van de data plaatsvind in het OnLoad event, welke plaatsvind nadat de dropdown zijn geselecteerde waarde al heeft geladen. Wanneer dus opnieuw databinding plaatsvind, worde deze waarden weggegooid. Dit betekent dat als een gebruiker California selecteert en dan op de submit button klikt, de dropdown de standaard item retourneert. Dit is standaard het eerste item, tenzij anders aangegeven. De oplossing om dit probleem te verhelpen is eenvoudig: Verplaats de databind naar het OnInit event:


protected override void OnInit(EventArgs args) {

this.lstStates.DataSource = QueryDatabase();
this.lstStates.DataBind();
base.OnInit(e);

}
Waarom dit wel werkt is omdat de populatie van de dropdown plaatsvind voordat de dropdown zijn geposte waarde laadt. Het werkt nu dus hetzelfde als in het eerste voorbeeld, alleen met dat verschil dat de ListItems niet worden opgeslagen in de ViewState. Deze oplossing geldt voor elke data die goedkoop en makkelijk te verkrijgen is. Je zou kunnen zeggen dat elke keer de pagina laadt een verbinding met de database ten koste gaat van de performance, in plaats van de data op te slaan in de ViewState. In dit geval is dit niet het geval. Moderne databases (zoals bijv. SQL Server) hebben een gecompliceerd caching mechanisme en zijn efficient geconfigureerd. De statenlijst moet gevuld worden bij elke request, ongeacht wat je doet. Bovendien staat een WebServer altijd dichtbij de DatabaseServer. Dit is dus sneller dan het verkeer (ViewState) dat plaatsvindt tussen de client en de webserver.

Bron: http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx

Reacties

Populaire posts van deze blog

[SQL Server] varchar vs nvarchar

MS Sql 70-461: Chapter 5

[C#] Class serialiseren en deserialiseren