ViewState – z czym to się je
Postaram się przeprowadzić małą analizę jednego z mechanizmów przechowywania stanów, jakie dostarcza ASP.NET. Mam nadzieję, że poniższy opis ViewState – jego konstrukcji, sposobu przechowywania i zadań jakie spełnia – pozwoli uniknąć kilku problemów podczas programowania. Starałem się wybrać rzeczy najbardziej istotne, zbudować zwarty „przekrój przez ViewState”. Oczywiście wyczerpanie tematu jest niemożliwe, jednak opis ten stanowić może punkt zaczepienia do dalszych rozważań czy poszukiwań.
ASP.NET udostępnia trzy mechanizmy zapamiętywania stanu. Należą do nich:
Application State
Wspólny dla całej aplikacji obiekt typu HttpApplication, dostępny w każdej chwili przez zmienną Application. Stanowi zbiór obiektów definiowanych przez użytkownika, których czas życia jest taki jak czas życia całej aplikacji. Kolekcja Application dostępna jest od czasu wykonania zdarzenia Application_OnStart – czyli wtedy, gdy po uruchomieniu aplikacji na serwerze pierwszy użytkownik wykona do niej żądanie. [1]
Session State
Wspólny dla danej sesji użytkownika. Jest to instancja klasy HttpSessionState, unikalna dla każdej sesji użytkownika i wspólna dla wszystkich jego żądań. Jest to również kolekcja obiektów, niszczona automatycznie po określonym w Web.config czasie braku aktywności użytkownika. Gdu użytkownik inicjuje żądanie po raz pierwszy, serwer tworzy obiekt Session i wysyła do użytkownika jego unikalny identyfikator. Domyślnie identyfikator ten jest zapisywany w cookies, a gdy te nie są dostępne – doklejany do URL każdej strony. Oczywiście można przełączać aplikację między tymi trybami ustawieniami parametru cookieless w web.config. ASP.NET oferuje trzy metody przechowywania sesji, nie to jest jednak przedmiotem wpisu, dlatego polecam doczytać o trybie InProc, StateServer oraz SqlServer.[2]
View State
Czyli nasz dzisiejszy bohater. ViewState to jedna z wyjątkowych cech ASP.NET. To swoista „umiejętność” zapamiętywania stanu kontrolek na konkretnej stronie. Ten „stan widoku” jest generowany przy pierwszym wczytaniu strony aspx i wysyłany do klienta (przeglądarki) w postaci ukrytego pola o nazwie „__VIEWSTATE”. Pole to można znaleźć na każdej stronie stworzonej w ASP.NET (w różnej formie, ale o tym później).
ViewState jest atrybutem typu StateBag klasy System.Web.UI.Control, z której dziedziczą wszystkie kontrolki (w tym strony – czyli instancje klasy System.Web.UI.Page).
Do czego właściwie służy ViewState?
Zastanawiacie się pewnie dlaczego cała ta zabawa z ukrywanym polem, do czego jest to w ogóle potrzebne? Otóż w ASP.NET każde odwołanie do strony powoduje utworzenie jej nowej instancji. Dzieje się tak dlatego, że protokół HTTP jest bezstanowy Siłą rzeczy więc, cykl życia zmiennych zadeklarowanych w kodzie kończy się z chwilą przekazania strony do klienta. Jak w takim razie przechowywać informacje o stanie kontrolek – danych w nich wyświetlanych czy ich właściwościach? Właśnie od tego jest ViewState – zdejmuje z programisty obowiązek zarządzania stanem każdej kontrolki na stronie.
Jakby to było, gdyby go nie było? ;-)
Wyobraźmy sobie sytuację, w której ViewState nie istnieje. Ściągamy z bazy danych informacje, których zażądał użytkownik – np. listę jednostek organizacyjnych – i wyświetlamy ją przy użyciu kontrolki Repeater. Gdyby nie ViewState, przy każdym ponownym odświeżeniu tej samej strony – czyli postbacku – wywołanym przez użytkownika choćby naciśnięciem przycisku, dane musiałyby być ponownie ściągane. Przyznacie, że nie jest to sytuacja komfortowa. ViewState pozwala na zapamiętanie ściągniętych danych i ich odtworzenie przy każdym kolejnym postbacku. Nadmienić należy, iż każda strona posiada property IsPostback, które mówi o tym, czy została ona pokazana po raz pierwszy, czy jest to kolejne odwołanie do niej samej.
Każda kontrolka posiada również property EnableViewState, które pozwala na włączanie lub wyłączanie ViewState’u na niej. Domyślnie ViewState zawsze jest włączony.
W celu zobrazowania przytoczonej powyżej sytuacji (czyli braku ViewState), rozpatrzmy przykład:
umieszczamy na stronie kontrolkę Label z property EnableViewState ustawionym na false oraz Button, który nie wykonuje żadnego konkretnego zadania poza wywołaniem postbacku.
Przy pierwszym załadowaniu strony wpisujemy do kontrolki lblDate aktualną datę – datę pierwszego wyświetlenia tej właśnie strony przez użytkownika:
{
if (!IsPostBack)
{
lblDate.Text = DateTime.Now.ToShortDateString();
}
}
Efekt po pierwszym wywołaniu strony:
Jak widać, wykonał się kod w warunku if(!IsPostback), czyli „gdy jest to pierwsze odwołanie do strony”. Jeśli jednak wywołamy postback strony przyciskiem „Akcja”, efekt będzie taki:
Co się stało? Ponowne odwołanie do tej samej strony powoduje, że property IsPostback ma wartość true, więc kod wpisania daty się nie wykonuje. Jednocześnie pozbawiliśmy kontrolkę Label jej wewnętrznej pamięci – poprzednia wartość więc się resetuje i property Text kontrolki jest puste. Gdybyśmy chcieli utrzymać wartość wyświetlaną w kontrolce, najlepszym wyjściem byłoby zachowanie jej wartości w ukrytym polu formularza i odtworzenie przy odwołaniu do strony – czy nie kojarzy się to wam z PHP ;-) ? Właśnie – Microsoft zadbał, by w ASP.NET nie trzeba było martwić się o stan kontrolek i podarował nam ViewState.
Gdybyśmy w powyższym przykładzie zmienili tylko jedną rzecz – property EnableViewState kontrolki Label na wartość true – wielokrotne wywoływanie postbacku przyciskiem „Akcja” nie spowodowałoby zniknięcia/zresetowania raz ustawionej daty. Oczywiście do czasu, gdybyśmy jawnie sami nie zmienili jej wartości. Prawda, że wygodne?
Jak przechowywany jest ViewState?
Jak wspomniałem wczesniej, ViewState domyślnie przechowywany jest na stronie w ukrytym polu. Wysyłany jest więc do przeglądarki z każdym odwołaniem. Jego przykładowy wygląd:
Jak widać nie jest to czysty tekst. Wartość pola kodowana jest alorytmem base64, czyli kodowaniem określanym mianem transportowego. Jest to kodowanie, nie szyfrowanie dlatego należy być ostrożnym w umieszczaniu istotnych, prywatnych danych w kolekcji ViewState. ViewState może być bardzo prosto odkodowany choćby przy użyciu tego dekodera online. Microsoft w oficjalnym artykule prezentuje sposób na parsowanie ViewState’u, na CodeProject natomiast znaleźć możemy artykuł, w którym prezentowana jest realizacja prostej przeglądarki ViewState’u. O zabezpieczeniach, szyfrowaniu ViewState czytaj niżej.
ViewState posiada jeszcze jedną nie do końca dogodną cechę. Tak jak za każdym żądaniem jest wysyłany do klienta, tak przy każdym wywołaniu postbacku musi być odesłany z powrotem na serwer. O ile nie stwarza to problemów przy małych stronach (z niewielką ilością danych) o tyle staje się istotne, gdy ViewState zaczyna ważyć kilka dziesiątek lub setek kilobajtów. Staje się to dodatkowym narzutem do przetransportowania. Na końcu artykułu poruszana jest kwestia wielkości ViewState, prezentowane są także alternatywne metody jego przechowywania.
Jak przechowywać własne obiekty w ViewState
Odpowiedź jest dość prosta – zapewnijmy możliwość serializacji i dodajmy do kolekcji.
Przypuśćmy, że chcemy przechować w ViewState instancję klasy Person:
public class Person
{
public Person(int id, string name, int age)
{
this._id = id;
this._name = name;
this._age = age;
}
private int _id;
public int Id
{
get { return _id; }
set { _id = value; }
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
private int _age;
public int Age
{
get { return _age; }
set { _age = value; }
}
}
O czym trzeba pamiętać?
- Kolekcja ViewState przechowuje obiekty – niezbędne jest więc jawne rzutowanie
- Przed „wyciągnięciem” obiektu i zrzutowaniem go należy sprawdzić, czy napewno istnieje w kolekcji – czyli czy dla danego klucza typu string wartość jest różna od null (patrz przykład poniżej)
- Oznaczenie klasy atrybutem [Serializable]
Wykonanie tych trzech rzeczy gwarantuje wolną od błędów obsługę przechowywania własnych obiektów w ViewState. Poniżej poprawny przykład:
{
get { return ViewState["MyPerson"] == null ? null : (Person)ViewState["MyPerson"]; }
set { ViewState["MyPerson"] = value; }
}
Gdybyśmy jawnie nie wykonali sprawdzenia, czy wartość w kolekcji dla klucza „MyPerson” nie jest równa null i od razu przeprowadzili rzutowanie – jeden z „ulubionych” wyjątków typu NullReference miałby dużą szansę nas zaskoczyć (np. odwołanie przed wpisaniem danych). Z kolei nie oznaczenie klasy jako serializowalnej spowoduje wystąpienie wyjątku HttpException.
Dodatkowo ViewState oferuje kilka metod:
- Clear – czyści cały ViewState
- Add – dodaje lub (jeśli wpis o takim kluczu istnieje) aktualizuje dane w kolekcji
- Remove – pozwala na usunięcie elementu o danym kluczu z kolekcji
- IsItemDirty – pozwala na pobranie informacji, czy element o zadanym kluczu został zmieniony podczas przetwarzania ostatniego żądania
Kiedy ViewState jest zapisywany, a kiedy odtwarzany
ViewState zapisywany jest zawsze po zdarzeniu Page_Load (i ewentualnym wykonaniu wszystkich zdarzeń postbacku), przed fazą Render. Oznacza to, że ostatnim miejscem, w którym możemy dokonać zmiany wartości zapisywanych w ViewState jest zdarzenie Page_PreRender. Przed zrenderowaniem strony rekurencyjnie zapisywany jest stan wszystkich kontrolek na stronie, tak aby mógł zostać przesłany jako ukryte pole.
Odtwarzanie ViewState’u występuje tylko przy postbacku, zaraz po inicjalizacji (Page_Init) – dzięki temu w Page_Load mamy już zawsze odtworzony stan strony z ostatniego żądania. Należy pamiętać, że w zdarzeniu Page_Init nie można odczytywać ani wpisywać niczego do ViewState’u – nie jest on jeszcze odtworzony.
Kilka (ważnych) słów o bezpieczeństwie
Na ViewState składają się trzy części (określane jako Triplet):
- Hash strony – używa tzw. machine authentication check (MAC). Dołączany dodatkowy hasha na końcu ViewState’u, ograniczający możliwość jego modyfikacji. Jest to dodatkowa suma kontrolna, obliczana z wartości dwóch niżej wymienionych wartości.
- Zserializowane stany kontrolek – czyli „rdzeń” ViewState’u, surowe dane
- Tablica kontrolek – zawiera listę kontrolek, z których zbudowano ViewState. Strona musi dostarczyć przy postbacku dokładnie tą samą kolekcję kontrolek, w przeciwnym przypadku wystąpi błąd.
Złączenie tych trzech elementów jest kodowane base64 i wysyłane na stronę.
Domyślnie dołączanie MAC jest włączone (przez property EnableViewStateMac). Jak jednak dowiedzieć można się z dokumentacji Microsoftu, czasem może powodować to dziwne zachowanie przy korzystaniu z metody Server.Transfer(). Ustawienie wskazanego property na false rozwiązuje problem, powoduje jednak powstanie dziury bezpieczeństwa – ViewState może zostać odkodowany, zmieniony i odesłany spowrotem do serwera.
Domyślnie do obliczania hasha z użyciem MAC używany jest algorytm SHA1, jednak można zmienić go za pomocą wpisu w web.config.
Przy postbacku następuje odkodowanie ViewState’u i porównanie hasha z właściwymi danymi. Wykrycie modyfikacji spowoduje błąd FormatException lub HttpException.
Szyfrowanie – czy to możliwe?
Tak – możliwe. Jednak należy zadać sobie pytanie, czy skoro myślimy już o szyfrowaniu, to czy na pewno umieszczamy w ViewState dane, które znaleźć się tam powinny? W żadnym razie nie należy w ViewState przechowywać danych, których modyfikacja lub pozyskanie może spowodować niepożądane konsekwencje.
Włączenie szyfrowania polega na odpowiednim ustawieniu wartości elementu machineKey w web.config, w sekcji
Uwaga – szyfrowana oczywiście nie jest zawartość ViewState a jedynie sam klucz, służący do weryfikacji.
Możliwe jest dołączenie dodatkowego klucza – przy użyciu property strony – ViewStateUserKey. Najlepiej ustawiać wartość (string) tego klucza w zdarzeniu Page_Init. ASP.NET używa go jako parametru wejściowego do algorytmu hashującego, który generuje machine authentication check.
Wielkość ViewState
Nie należy zapominać o tym, że ViewState wprowadza dodatkowy narzut transportowy. Serwer musi wysłać do użytkownika całą stronę i dodatkowo ViewState, natomiast użytkownik odsyła na serwer wartość ukrytego pola przy każdym postbacku. Rozważmy prosty przykład. Na stronie umieścimy kontrolkę Repeater, stworzymy sztuczną listę instancji obiektów typu Person (kod klasy dostępny wyżej). Następnie podłączymy listę jako źródło danych do repeatera i wyświetlimy na stronie.
Kod w pliku aspx:
<ItemTemplate>
<%# DataBinder.Eval(Container.DataItem, "Id") %>
<%# DataBinder.Eval(Container.DataItem, "Name") %>
<%# DataBinder.Eval(Container.DataItem, "City") %>
<%# DataBinder.Eval(Container.DataItem, "Age") %>
</ItemTemplate>
<SeparatorTemplate>
<br />
</SeparatorTemplate>
</asp:Repeater>
Kod w pliku aspx.cs:
{
if ( !IsPostBack )
{
List<Person> list = new List<Person>();
for ( int i = 0; i < 50; i++ )
{
list.Add( new Person( i + 1, "Name MyName", i + 20 ) );
}
repPersons.DataSource = list;
repPersons.DataBind();
}
}
Jak widać kolekcja liczy 50 osób i przy pierwszym pokazaniu strony podpinana jest do repeatera. Najprostszym sposobem na sprawdzenie wielkości ViewState’u jest wyświetlenie długości ukrytego pola przy użyciu JavaScriptu. To proste zadanie realizuje poniższy kod:
var field = document.forms[0]["__VIEWSTATE"].value;
alert("Rozmiar ViewState (kb): " + field.length / 1024);
}
Który podłączamy do przycisku na stronie:
W moim przypadku wynik wygląda następująco:
Cała strona waży 5,6kB, ViewState – 2,9kB. Daje to aż 51% wagi całej strony!
Jak pokazuje powyższy bardzo prosty przykład, należy być ostrożnym przy używaniu ViewState’u z kontrolkami, które są pojemnikami na dane, wyświetlają ich znaczną ilość. W literaturze spotyka się zdania, że jeśli waga ViewState’u przekracza 30% wagi całej strony, trzeba pomyśleć o zmianie sposobu przechowywania jego wartości lub inaczej obsłużyć kontrolki, które powodują tak duży jego przyrost. Oczywiście należy podchodzić do tego zagadnienia racjonalnie – jeśli są to takie wielkości jak w przytoczonym przykładzie, nie należy sobie zawracać tym głowy. Jeśli jednak strona waży 200kB-300kB, z czego waga ViewState’u to 50-60% – najwyższy czas pomyśleć o optymalizacji.
Mam nadzieję, że udało mi się przybliżyć temat ViewState’u. Oczywiście poruszone są najważniejsze kwestie, z którymi spotkamy się w codziennym programowaniu. O ViewState można by napisać porządny rozdział w książce, przyznacie jednak, że nie do końca o to chodzi.
Postaram się w kolejnym wpisie poruszyć kwestię przechowywania ViewState’u w postaci innej niż ukryte pole formularza. Możliwości jest kilka – od sesji, przez bazę danych aż po pliki.
Po uporządkowaniu i skomentowaniu kodu źródłowego dołączę go do tego artykułu.
Źródła:
http://www.extremeexperts.com/Net/Articles/ViewState.aspx
http://msdn.microsoft.com/en-us/library/ms972976.aspx#viewstate_topic3
http://www.codeproject.com/KB/viewstate/viewstate_viewer.aspx
http://blog.sb2.fr/post/2008/12/25/Storing-ViewState-On-Server-Instead-Of-Client-With-ASPNET.aspx
http://aspalliance.com/72
http://msdn.microsoft.com/pl-pl/magazine/cc188774(en-us).aspx
Przypisy
- http://msdn.microsoft.com/en-us/library/bf9xhdz4(VS.71).aspx↩
- http://msdn.microsoft.com/en-us/library/87069683(VS.71).aspx↩
Nie znaleziono podobnych wpisów.
Możesz śledzić odpowiedzi do tego wpisu za pomocą RSS 2.0 feed. Możesz leave a response, or trackback z Twojej własne strony.








ViewState – z czym to się je | Blog o programowaniu C#, ASP.NET…
Dziękujemy za publikację – Trackback z dotnetomaniak.pl…
[...] Gdyby ktoś nie był zbyt obeznany w działaniu ViewState, zapraszam do przeczytania wpisu na blogu http://andrzej.net.pl/ Pozdrawiam, [...]
dzieki, fajne podreczne kompendium
Nie jestem zorientowany w tematyce SHA-1 i wobec tego moje pytanie może zabrzmieć dziwnie. Czy mógłbyś wyjaśnić w jaki sposób obliczany jest MAC lub skierować mnie pod odpowiedni adres ?
Niestety nie mam pojęcia jak obliczany jest MAC.
„Ślady”, które mogę podsunąć nie mówią zbyt wiele:
http://msdn.microsoft.com/en-us/library/ms972976.aspx
http://msdn.microsoft.com/en-us/library/w8h3skw9.aspx
http://msdn.microsoft.com/en-us/library/ms998288.aspx