Snad žádná jiná technologie v ASP.NET nezpůsobila tolik zlé krve jako právě ViewState. Hromady na první pohled zbytečných a nesmyslných Base64 kódovaných dat ve skrytém formulážovém poli __VIEWSTATE jsou to první, na co si obvykle odpůrci ASP.NET Web Forms vzpomenou, často s uštěpačnou poznámkou v duchu "nojo, co byste chtěli od Microsoftu". Jako u každé technologie ovšem i zde platí, že nejsou technologie dobré a špatné, ale pouze vhodně a nevhodně použité.

Technologie ViewState a její věrná sestřička ControlState jsou ve Web Forms přítomny proto, aby pomohly obcházet bezstavovost HTTP. Tomuto tématu jsem se již na stránkách ASP.NET věnoval, takže pro obecnější úvod do problematiky se můžete podívat na starší články. Byla v nich řeč i o ViewState, ale spíše z pohledu autora stránky. V tomto seriálu se podíváme na ViewState poněkud detailněji, také z pohledu autora komponenty.

ViewState

Kolekce ViewState je úložiště, do nějž si programátor (typicky autor nějakého serverového ovládacího prvku) můýže uložit libovolnou serializovatelnou hodnotu. A budou-li bohové milostiví, při postbacku, tedy odeslání stránky, tam tuto hodnotu zase najde. Typicky se tato technika používá u složitějších prvků a formulářů, kde chceme zachovávat větší množství stavových hodnot u prvků, které za sebou nemají nativní formulářové pole.

Představme si například, že chceme z nějakého důvodu vědět, kdy byla zobrazena stránka, kterou posléze odesíláme. Tento čas prvního požadavku chceme zachovat na věky věkův – a pokud to nejde, tak alespoň tak, aby přežil postbacky tohoto formuláře. Zdatným pomocníkem v tomto směru nám bude třída, resp. server control SampleStateControl.

using System;

using System.Web;

 

namespace MyControls {

 

    public class SampleStateControl : System.Web.UI.Control {

 

        public DateTime FirstLoadTimeVS {

            get { return (DateTime)this.ViewState["FirstLoadTimeVS"]; }

            set { this.ViewState["FirstLoadTimeVS"] = value; }

        }

 

        protected override void OnLoad(EventArgs e) {

            base.OnLoad(e);

 

            if (!this.Page.IsPostBack) {

                // Jedná se o první dotaz, ne o postback

                this.FirstLoadTimeVS = DateTime.Now;

            }

        }

 

        protected override void Render(System.Web.UI.HtmlTextWriter writer) {

            if (!this.Page.IsPostBack) {

                // Jedná se o první dotaz, ne o postback

                writer.Write("<p>Toto je první dotaz.</p>");

            }

            else {

                // Jedná se o postback

                writer.Write("<p>ViewState: první dotaz nastal v {0}.</p>", this.FirstLoadTimeVS);

            }

        }

 

    }

 

}

Jádrem všeho jest vlastnost FirstLoadTimeVS. Ta se stará  práci s kolekcí ViewState. Zde uvedený kód je pro běžné "properties" server controls tak běžný, že jsem si na něj dokonce udělal code snippet, jak ho píšu často. Na názvu oné položky kolekce nezáleží, důležité je pouze aby byl unikátní v rámci jednoho konkrétního controlu. Obvykle se tedy používá název té které vlastnosti. Ve metodě OnLoad tuto vlastnost inicializujeme: nejedná-li se o postback, uložíme do ní aktuální datum a čas. Metoda Render pak vypíše příslušné hodnoty.

Ovládací prvek je hotov, takže si vytvoříme stránku, ve které ho použijeme:

<%@ Page Language="C#" %>

<%@ Register TagPrefix="sample" Namespace="MyControls" %>

<!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>

</head>

<body>

    <form id="form1" runat="server">

    <sample:SampleStateControl ID="SampleStateControl1" runat="server" />

    <asp:Button ID="Button1" runat="server" Text="Odeslat" />

    </form>

</body>

</html>

Když si stránku zobrazíme, při prvním požadavku se ukáže zpráva "Toto je první dotaz." a uloží se aktuální čas. Při postbacku se potom tento čas zobrazí a zůstává zachován i pro další postbacky, tlačítko "Odeslat" můžete mačkat opakovaně a hodnota se nezmění. Je uložena ve skrytém poli formuláře, v HTML kódu stránky, který si můžeme zobrazit v prohlížeči:

<!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>

    <title></title>

</head>

<body>

    <form name="form1" method="post" action="default.aspx" id="form2">

    <div>

        <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUJNjE0NDc2NzQ5D2QWAgIDD2QWAgIBDxYCHg9GaXJzdExvYWRUaW1lVlMG0Tw19qvAy4hkZLVVfrxY9sbfkkb51jVYKnmfQPMK" />

    </div>

    <div>

        <input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWAgL/18YvAoznisYGeTQeng+MgQTEfJxY5SSWGNJ7qds=" />

    </div>

    <p>Toto je první dotaz.</p>

    <input type="submit" name="Button1" value="Odeslat" id="Submit1" />

    </form>

</body>

</html>

Obsahu skrytého pole __VIEWSTATE se budeme důkladně věnovat později, pro tento okamžik se spokojte s mým ujištěním, že aktuální čas je pečlivě zaznamenán právě zde.

Vypnutí ViewState

ViewState lze zakázat. Každý server control i stránka sama (která je jenom speciálním případem controlu) má vlastnost EnableViewState, kterou lze nastavit na false a v takovém případě se shora popsaný mechanismus nepoužije. ViewState můžete vypnout i pro celou akci pomocí web.configu, když nastavíte /configuration/system.web/pages/@enableViewState na false.

Pokud ViewState na úrovni stránky nebo aplikace vypneme a stránku si nově zobrazíme, skryté pole __VIEWSTATE sice nezmizí, ale jeho obsah se výrazně zmenší. Zbavit se tohoto pole úplně je civilizovanými prostředky téměř nemožné a především to není moc účelné. Kromě hodnot uložených způsobem popsaným výše se v něm skrývají i hodnoty ControlState (o tom budeme mluvit později) a také podle něj ASP.NET identifikují postback, tedy že vůbec došlo k odeslání formuláře. Naším cílem není se ViewState úplně zbavit, ale využívat ho smysluplně.

Smutnou skutečností je, že aktuální verze ASP.NET (3.5 SP1) umožňuje ViewState selektivně vypínat, ale už ne zapínat. Pokud ViewState na vyšší úrovni zakážete, už ho na nižší nepovolíte. Toto omezení bude odstraněno v nadcházející verzi 4.0.

Máme tedy zobrazenu stránku se zakázaným ViewState a stiskneme odesílací tlačíko. Stránka promptně vyhlásí populární NullReferenceException na následujícím kódu:

return (DateTime)this.ViewState["FirstLoadTimeVS"];

Pokud je funkce ViewState vypnutá, tak kolekce ViewState funguje dále (tj. v průběhu jednoho životního cyklu stránky ji můžeme nadále využívat), ale nepřežije postback, nebude mít po něm inicializované žádné položky a dotaz na libovolnou z nich vrátí null. Protože DateTime je struktura (struct), není možné null takto přetypovat a proto dojde k výše popsané výjimce. Do ViewState tedy ukládejte pouze nulovatelné typy (jako například DateTime? alias Nullable<DateTime>) a nebo v konstruktoru vlastnosti inicializujte na nějakou výchozí hodnotu, například takto:

public SampleStateControl() {

    // Nastavení výchozí hodnoty

    this.FirstLoadTimeVS = DateTime.MinValue;

}

Pokud chybu takto odstraníme, stránka úspěšně funguje, ale tvrdí, že první dotaz nastal 01. 01. 00001 v 00:00:00, což je hodnota DateTime.MinValue a tedy důkaz, že se prvotní čas nikde neuložil.

Morální ponaučení

Morální ponaučení ze shora uvedeného příběhu zní: do ViewState si ukládejte pouze to, co můžete postrádat. Co jste schopni získat v případě potřeby nějak jinak. ASP.NET se tak chová třeba při data bindingu: aby nemusel při odeslání formuláře zatěžovat databázový server, tak si dříve zjištrěné hodnoty uloží do ViewState a po postbacku je použije. Můžete si tedy vybrat mezi zvýšením zátěže databáze a nebo zvýšením objemu přenesených dat.

Co ale pokud si některé věci potřebujete uložit opravdu nutně? A chcete, aby control fungoval i při vypnutém ViewState? Existuje řešení? Dobrá zpráva je: ano, existuje a jmenuje se ControlState. Špatná zpráva je, že více se o něm dozvíte až v příštím článku.