Introduction
Comprendre le
viewstate
Enregistrer des valeurs
Détecter les modifications de ViewState
Sérialiser et désérialiser
Restaurer automatiquement les données
Optimiser
la taille du viewstate
ViewState ne
gère pas l'état, vraiment?
D'autres viewstate
Bibliographie
Le viewstate, Ah oui, c'est pas le mécanisme qui assure la persistance des saisies lors d'un renvoi d'une page?
Cette phrase banale et mainte fois entendue résume à elle seule l'incompréhension profonde qu'ont la majorité des développeurs envers le viewstate.
Cette incompréhension draine avec elle de nombreux mythes, par exemple la malédiction qui veut que l'on doivent absolument choisir entre la bande passante et le viewstate. Ce n'est pas forcément inexacte, mais réducteur car ce faisant on ne se rend pas compte que souvent le viewstate est surtout rempli de données absolument inutiles.
C'est en lisant un extraordinaire article de Dave Reed[15] que j'ai réalisé à quel point moi aussi j'étais en dehors du coup. L'article que j'espère vous lirez est le résultat de cet étonnement et il a profondément modifié ma façon d'écrire des pages ASP.
Je ne vais pas beaucoup parler de la structure du viewstate ou d'anecdotes de ce type, mais uniquement d'un point précis: que se passe t'il dans les coulisses?
On abordera ensuite une application pratique directe: peut t'on optimiser le volume du viewstate ?
Comme vous le constaterez je vais beaucoup m'inspirer de l'article de Reed, à commencer par en reprendre le plan. Il ne s'agit en rien d'une traduction, j'ai adapté, ajouté des choses et retiré d'autres. Donc n'hésitez pas à lire l'article d'origine, il est rédigé dans un style assez inhabituel pour les blogueurs de chez Microsoft!
Bonne lecture!
Pour ceux qui se posent des question, le Gray's Anatomy est un ouvrage d'anatomie important de la littérature médicale anglo-saxonne dont la première publication date de 1858. Vous pensiez à quoi d'autre?
Beaucoup de développeurs sont convaincus que le viewstate est responsable du mécanisme de persistance des composants ASP. C'est faux dans la plupart des cas.
L'expérience suivante devrait vous en convaincre:
Le viewstate n'est pas responsable de la persistance des données
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" EnableSessionState="False" %> <!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" > <body> <form id="form1" runat="server"> <div> <asp:textbox ID="TextBox1" runat="server" EnableViewState="False"></asp:textbox> <asp:button ID="Button1" runat="server" EnableViewState="False" Text="Button" /></div> </form> </body> </html>Une page avec un TextBox et un bouton. Tous les composants présents (y compris la page elle même) ont la propriété EnabledViewState à false.
La persistance est essentiellement due au mécanisme de POST des pages HTML, mais nous verrons que malgré tout le viewState joue un rôle dans cette affaire..
Une autre idée reçue est que le viewstate est une machine à plomber la bande passante. C'est pas entièrement faux, mais mérite d'être nuancé. Tout d'abord avec ASP 2.0 Microsoft a introduit des optimisations du format de la sérialisation du viewstate qui se traduit par une diminution jusqu'à 50% de sa taille. Par ailleurs la taille excessive du viewstate de certaines pages est souvent du à une mauvaise compréhension de son fonctionnement qui conduit à y placer des données absolument inutiles. L'optimisation du viewstate est aussi un des sujets que nous couvrirons dans cette partie.
Le viewstate rempli 4 fonctions précises qui jouent chacune un rôle dans le fonctionnement d'ASP:
Le viewstate est souvent réduit au champ caché __VIEWSTATE que l'on trouve dans les pages ASP. En fait il ne s'agit que de la partie émergée d'un iceberg un peu plus gros.
Le viewstate c'est un objet implémentant
IStateManager et un ensemble de mécanismes
pris en charge par ASP.
IStateManager est implémenté par de nombreux
composants de l'espace de noms WebControls
comme MenuItem, ListItem,
DataGridColumn, DataKey...
La principale exception est la propriété Style qui implémente une instance de IStateManager différente de celle de StateBag. Le reste est semblable.
IStateManager expose une propriété et 3 méthodes:
Code C#
Code VB
Nous verrons au cours de ce chapitre le rôle de ces membres.
Pour ce qui nous intéresse ici le viewstate est
monté autour de la classe concrète StateBag.
Cette classe est exposée par la propriété
ViewState héritée de Control.
Si vous vous amusez à décompiler quelques composants
avec Reflector[16] vous découvrirez vite que quasiment
toutes les propriétés des contrôles sont chargées dans
ViewState.
Par exemple la propriété Text d'un Button est implémentée ainsi:
Implémentation de Button.Text
Code C#
public string Text { get { string text1 = (string) this.ViewState["Text"]; if (text1 != null) { return text1; } return string.Empty; } set { this.ViewState["Text"] = value; } }
Code VB
Public Property [Text] As String Get Dim text1 As String = CStr(Me.ViewState.Item("Text")) If (Not text1 Is Nothing) Then Return text1 End If Return String.Empty End Get Set(ByVal value As String) Me.ViewState.Item("Text") = value End Set End Property
Notez en particulier le traitement de la valeur par défaut. On ne l'ajoute pas dans ViewState, on se contente de la retourner. Il s'agit d'une optimisation dont les raisons apparaîtront plus claires ultérieurement. Pour ces mêmes raisons il est peu conseillé d'ajouter une ligne telle MonButton.Text = "" quelque part dans le code.
Du point de vue fonctionnel, StateBag se
comporte
comme une table de hachage.
Chaque composant serveur posé sur une page ASP est
associé à sa propre instance de Statebag. Le
fait d'ajouter runat="server" à un contrôle
HTML créé également une instance de StateBag.
Pour finir sachez que rien ne vous interdit d'utiliser ViewState comme espace de stockage de n'importe quelle valeur d'état du contrôle.
La seule contrainte, est que l'objet placé dans le viewstate soit sérialisable[28]. Soit parce qu'il implémente ISerializable, ou bien est décoré de l'attribut SerializableAttribute ou encore parce qu'il implémente un TypeConverter. Les classes qui disposent d'un TypeConverter donnent un viewstate plus compact[50]
On remarque l'analogie entre ViewState et les objets Session ou Application. mais il y a une différence essentielle: leur portée. On peut résumer les choses ainsi:
| Application | La portée est celle de
l'instance en cours d'ASP. Application joue un rôle similaire aux propriétés static (shared en VB). Tant que le processus ASP n'est pas relancé Application conserve son contenu qui peut être partagé entre plusieurs sessions. On utilise typiquement Application pour charger des valeurs de référence dans un site ou enregistrer des informations comme le nombre de personnes connectées dans un chat. |
| Session | La portée est la session
ouverte par l'utilisateur connecté. Session peut échanger des informations entre plusieurs pages, mais pas d'un utilisateur à l'autre. |
| ViewState | La portée est la page en cours. On ne peut échanger un ViewState entre deux pages. |
On aborde un point très méconnu, mais crucial.
StateBag est par nature une table de hachage, mais l'examen de sa méthode Add() va nous révéler quelque chose de plus.
Source de StateBag.Add()
Code C#
public StateItem Add(string key, object value) { if (string.IsNullOrEmpty(key)) { throw ExceptionUtil.ParameterNullOrEmpty("key"); } StateItem item1 = this.bag[key] as StateItem; if (item1 == null) { if ((value != null) || this.marked) { item1 = new StateItem(value); this.bag.Add(key, item1); } } else if ((value == null) && !this.marked) { this.bag.Remove(key); } else { item1.Value = value; } if ((item1 != null) && this.marked) { item1.IsDirty = true; } return item1; } |
Code VB
Public Function Add(ByVal key As String, ByVal value As Object) As StateItem If String.IsNullOrEmpty(key) Then Throw ExceptionUtil.ParameterNullOrEmpty("key") End If Dim item1 As StateItem = TryCast(Me.bag.Item(key),StateItem) If (item1 Is Nothing) Then If ((Not value Is Nothing) OrElse Me.marked) Then item1 = New StateItem(value) Me.bag.Add(key, item1) End If ElseIf ((value Is Nothing) AndAlso Not Me.marked) Then Me.bag.Remove(key) Else item1.Value = value End If If ((Not item1 Is Nothing) AndAlso Me.marked) Then item1.IsDirty = True End If Return item1 End Function |
Les premières lignes ne font qu'enregistrer la valeur ajoutée, c'est la fin qui nous intéresse. Le code met à True une propriété IsDirty. Ce code n'est pas activé n'importe quand, il nécessite que la propriété private marked soit à true. Une nouvelle plongée dans le source de StateBag avec Reflector nous donne la deuxième pièce du puzzle:
Listing 6-4: Source de StateBag.TrackViewState()
Code C#
internal void TrackViewState() { this.marked = true; } |
Code VB
Friend Sub TrackViewState() Me.marked = True End Sub |
Nous voyons d'abord que ViewState n'est pas juste un espace de stockage. Chaque élément de la table fournit un mécanisme permettant de savoir s'il a été modifié depuis que la méthode TrackViewState a été appelé grâce à sa propriété IsDirty.
StateBag expose divers membres en liaison avec la propriété IsDirty:
| IsItemDirty | Obtient si le suivi est activé pour un élément donné du ViewState |
| SetDirty | Place à true l'état de suivi de tous les éléments de ViewState |
| SetItemDirty | Place l'état de suivit d'un élément à true ou false |
Faisons juste une remarque: il n'existe pas de propriétés ou de méthode publiques (au sens différent de private) qui permette de basculer à false le champ marked, donc de désactiver le suivi une fois activé. Par contre à l'aide de SetItemDirty on peut placer à false l'état de suivi d'un élément.
Avant de voir par qui et à quel moment TrackViewState est appelé, examinons la série de scénarios suivants pour être certain de comprendre ce qui se passe:
Le mécanisme de suivi en détail
Code C#
ViewState.IsItemDirty("clef"); // retourne FALSE, la clef n'existe pas
ViewState["clef"] = "abc";
ViewState.IsItemDirty("clef"); // retourne FALSE, la clef vient juste d'être créée
ViewState["clef"] = "def";
ViewState.IsItemDirty("clef"); // retourne FALSE, le suivi n'est pas activé
ViewState.TrackViewState(); // active le suivi
ViewState.IsItemDirty("clef"); // retourne FALSE, on n'a encore rien modifié
ViewState["clef"] = "ghi";
ViewState.IsItemDirty("clef"); // retourne TRUE, une modification s'est produite
ViewState.SetItemDirty("clef", false);
ViewState.IsItemDirty("clef"); // retourne FALSE
ViewState["clef"] = "jkl";
ViewState.IsItemDirty("clef"); // retourne TRUE, on ne peut pas désactiver le suivi
ViewState.SetItemDirty("clef", false);
ViewState.IsItemDirty("clef"); // retourne FALSE, évidemment!
ViewState["clef"] = "jkl";
ViewState.IsItemDirty("clef"); // retourne TRUE, bien que l'on remette la même valeur
Code VB
ViewState.IsItemDirty("clef") ' retourne FALSE, la clef n'existe pas
ViewState("clef") = "abc"
ViewState.IsItemDirty("clef") ' retourne FALSE, la clef vient juste d'être créée
ViewState("clef") = "def"
ViewState.IsItemDirty("clef") ' retourne FALSE, le suivi n'est pas activé
ViewState.TrackViewState() ' active le suivi
ViewState.IsItemDirty("clef") ' retourne FALSE, on n'a encore rien modifié
ViewState("clef") = "ghi"
ViewState.IsItemDirty("clef") ' retourne TRUE, une modification s'est produite
ViewState.SetItemDirty("clef", False)
ViewState.IsItemDirty("clef") ' retourne FALSE
ViewState("clef") = "jkl"
ViewState.IsItemDirty("clef") ' retourne TRUE, on ne peut pas désactiver le suivi
ViewState.SetItemDirty("clef", False)
ViewState.IsItemDirty("clef") ' retourne FALSE, évidemment!
ViewState("clef") = "jkl"
ViewState.IsItemDirty("clef") ' retourne TRUE, bien que l'on remette la même valeur
Note:
Cet exemple fait appel à une propriété internal
(friend) et n'est donc pas compilable sous
cette forme.
Notons aussi que le mécanisme de suivi n'est pas actif par défaut.
Lorsqu'une requête demande de charger une page ASP, celui-ci analyse son contenu à la recherche de composants portant l'attribut runat="server". ASP instancie alors le contrôle, puis examine par réflexion si des attributs ont été déclarés sur ce contrôle. Chaque attribut rencontré est alors ajouté au ViewState. Pour toute la suite on appellera valeur ViewState cette valeur initiale provenant de la déclaration dans la page.
Déclaration d'un bouton
<asp:button ID="Button1" runat="server" Text="Button" />
Dans notre exemple ASP trouvera par exemple l'attribut Text.
Cette analyse est réalisée très tôt dans le cycle de vie de la page, elle est réalisé avant le premier appel à TrackViewState. Par conséquent les éléments ajoutés à ce stade ont leur propriété IsDirty à false.
Les propriétés déclarées directement depuis le concepteur de page sont appelées propriétés déclaratives par opposition aux propriétés dynamiques qui sont chargées depuis le code.
TrackViewState est appelée immédiatement après l'initialisation, c'est à dire après l'appel à OnInit.
Cela signifie que l'on devient capable de différentier les propriétés déclaratives des propriétés dynamiques. Nous verrons au chapitre suivant le parti que l'on tirera de cette remarque.
L'événement Init marque le dernier moment où le suivi des modifications du Statebag n'est pas actif. Par conséquent pour charger des propriétés avant que le suivi du viewstate démarre, il suffit de surcharger la méthode OnInit. Cette remarque est la base des techniques d'optimisation de la taille du viewstate que nous examinerons plus loin.
L'ordonnancement des événements ASP fait que les événements Init des contrôles d'une page, s'effectuent avant celui de la page elle même. D'une façon générale les événements Init sont déclenchés en commençant par le bas de la hiérarchie des contrôles.
Le diagramme suivant résume la situation dans le contexte très simple d'une hiérarchie de contrôles sur 3 niveaux. Rappelez vous que l'on commence par le niveau le plus bas:

Vous avez probablement déjà examiné le rendu HTML d'une page ASP et découvert le champ caché __VIEWSTATE. Ce champ contient à l'évidence un objet sérialisé. Mais lequel?
Considérons la page suivante:
Hiérarchie des composants
<html> <body> <h1> Hello Amethyste</h1> <form id="form1" runat="server"> <asp:button ID="Button1" runat="server" Text="Button" /> <br /> Une liste ici <asp:dropdownlist ID="DropDownList1" runat="server"> <asp:listitem>Un</asp:listitem> <asp:listitem>Deux</asp:listitem> </asp:dropdownlist> </form> </body> </html>
Lorsqu'ASP analyse cette page il créé en interne une classe héritée de Page et pour l'essentiel composée d'une hiérarchie de composants qui se présentent représenter ainsi dans la collection Controls de la page[37]:

Cela a plusieurs implications. Par exemple le code suivant:
Création dynamique d'un composant
Code C#
protected void Page_Load(object sender, EventArgs e) { TextBox Texte = new TextBox(); this.Controls.Add(Texte); }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Dim Texte As TextBox = New TextBox Me.Controls.Add(Texte) End Sub
Ce code en apparence anodin provoque une exception à l'exécution.

TextBox est un composant qui a besoin de se trouver à l'intérieur d'une balise <form runat="server">. Ce point est vérifié par un appel à VerifyRenderingInServerForm dans la méthode Render du composant. Or le code l'ajoute au même niveau que HtmlForm et non pas dans sa collection de contrôle. D'où le message d'erreur. Un Label par exemple n'a pas de telle exigence et le code aurai parfaitement fonctionné si ce n'est que le rendu se trouverai après la balise </html> ce qui n'est de toute façon pas une bonne idée.
Il existe plusieurs façons de résoudre ce problème[32,33,37], mais la meilleure est tout de même de placer le composant directement au bon endroit:
Créer correctement un composant dynamique
Code C#
protected void Page_Load(object sender, EventArgs e) { TextBox Texte = new TextBox(); Page.FindControl("Form1").Controls.Add(Texte); }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Dim Texte As TextBox = New TextBox Page.FindControl("Form1").Controls.Add(Texte) End Sub
Vous pouvez aussi placer un PlaceHolder sur la page de façon à disposer d'une référence directe au bon emplacement depuis le code behind. Vous trouverez plus d'informations sur ce sujet en bibliographie [37].
Revenons à notre sujet et rappelons que chaque composant détient sa propre instance de StateBag.
IStateManager expose une méthode SaveViewState qui parcours récursivement la hiérarchie de contrôles. L'Object renvoyé par cette méthode n'est pas la grappe de contrôles, mais la grappe de données contenues dans ces contrôles. Seules les données dont la propriété IsDirty est à false seront recopiées. Les autres étant automatiquement reconstituées, elles ne nous intéressent pas.
Cet objet est ensuite sérialisé dans le champ caché __VIEWSTATE sous la forme d'une chaîne 64 bits éventuellement cryptée qui contient donc les données portées par les propriétés des contrôles de la page, ainsi que les divers états de la page. Pour information la classe responsable de la sérialisation/désérialisation d'un StateBag est LosFormatter en ASP 1 et ObjectStateFormatter en ASP 2.0. Il existe des utilitaires capables de visualiser le contenu de cette chaîne[43].
Les lecteurs malins voient déjà des perspectives se dessiner, patience, nous reviendront la dessus un peu plus loin. Je dois encore raconter la fin de l'histoire.
Avant cela signalons la nouvelle propriété Page.MaxPageStateFieldLength, que l'on peut aussi fixer dans web.config. Cette propriété fixe la taille maxi de __VIEWSTATE. Si cette taille est dépassée, alors le champ est découpé en autant de petits bouts que nécessaire. C'est une exigence de certains firewall ou proxy.
La suite est simple à deviner. La page se charge et les composants aussi. Avant même la phase d'initialisation ASP charge les propriétés déclaratives des contrôles. Survient alors Init, à un certain moment TrackViewState est lancé, puis LoadViewState est appelé. Le viewstate est désérialisé. Il contient les données anciennement marquées comme IsDirty. ASP réhydrate les propriétés des composants avec ce qu'il trouve dans le viewstate. Puisque le suivi est activé, elles seront à nouveau marquées comme IsDirty, prête à être persistées à nouveau.
Simple et brillant!
Signalons aussi que rien n'oblige à sérialiser le viewstate dans la page. D'autres conteneurs font aussi bien l'affaire. La classe PageStatePersister fournit les mécanismes de base pour implémenter un conteneur personnalisé[27,44].
Juste pour vérifier que tout est clair, regardez les deux pages suivantes:
Compréhension du viewstate
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <br /> <asp:label Text="Je pense, donc je suis" ID="Label1" runat="server" ></asp:label> <br /> <asp:button ID="Button1" runat="server" Text="Button" /> </form> </body> </html>
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <br /> <asp:label Text="Maître corbeau sur un arbre perché, tenait en son bec un fromage. Maître renard par l'odeur alléchée, lui tînt à peu près ce langage." ID="Label1" runat="server"></asp:label> <br /> <asp:button ID="Button1" runat="server" Text="Button" /> </form> </body> </html>
Ces formulaires sont identiques, seuls le texte affiché diffère, il est significativement plus long dans un cas.
Pour chaque formulaire on fait les deux expériences suivantes:
Répondez ensuite à la question suivante sans vous aider de Visual Studio, uniquement en examinant les listings :
Les valeurs obtenus diffèrent t'elles?
Si j'ai bien fais mon travail, si vous avez lu correctement les chapitres qui précèdent, la réponse est évidente. Sinon revenez en arrière.
Le viewstate présente l'inconvénient de devenir rapidement très volumineux et de ce fait consommer pas mal de bande passante. Il y a des raisons inévitables, mais il y a aussi des raisons liées à une mauvaise utilisation du viewstate. C'est ce point que nous allons aborder ici à travers des exemples typiques.
Vous souhaitez ajouter un Label sur toutes les pages pour afficher une information qui ne change pas durant le cycle de vie de l'application ou du moins de la session, comme le nom de la personne connectée, un message d'avertissement... Une façon de faire est la suivante:
Données statiques
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <asp:label ID="Qui" runat="server"></asp:label> <asp:button ID="Button1" runat="server" Text="Button" /> </form> </body> </html>
Code C#
protected void Page_Load(object sender, EventArgs e) { if (!this.IsPostBack) { this.Qui.Text = ChargerTexte(); } } protected string ChargerTexte() { return "Maître corbeau sur un arbre perché, tenait dans son bec un fromage"; }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) If Not Me.IsPostBack Then Me.Qui.Text = ChargerTexte End If End Sub Protected Function ChargerTexte() As String Return "Maître corbeau sur un arbre perché, tenait dans son bec un fromage" End Function
Tout fonctionne bien, mais à la lumière des explications précédentes vous savez maintenant que les modification de propriétés qui intervient dans Page_Load seront sérialisée dans le viewstate. Ce n'est que quelques octets certes, mais des octets quand même qui s'ajoutent à d'autres. Or cela est absolument inutile car retrouver l'information affichée n'est pas bien coûteux.
Voici plusieurs solutions.
La première idée est de désactiver le viewstate:
Données statiques, désactiver le viewstate
<html xmlns="http://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <asp:label EnableViewState="false" ID="Qui" runat="server"></asp:label> <asp:button ID="Button1" runat="server" Text="Button" /> </form> </body> </html>
Code C#
protected void Page_Load(object sender, EventArgs e) { this.Qui.Text = ChargerTexte(); }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Me.Qui.Text = ChargerTexte End Sub
On peut aussi charger le Label dans sa phase d'initialisation:
Données statiques initialisées dans Init:
<asp:label ID="Qui" runat="server" OnInit="Qui_Init"></asp:label>
Code C#
protected void Page_Load(object sender, EventArgs e) { } protected string ChargerTexte() { return "Maître corbeau sur un arbre perché, tenait dans son bec un fromage"; } protected void Qui_Init(object sender, EventArgs e) { Label Label = sender as Label; Label.Text = ChargerTexte(); }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) End Sub Protected Function ChargerTexte() As String Return "Maître corbeau sur un arbre perché, tenait dans son bec un fromage" End Function Protected Sub Qui_Init(ByVal sender As Object, ByVal e As EventArgs) Dim Label As Label = CType(ConversionHelpers.AsWorkaround(sender, GetType(Label)), Label) Label.Text = ChargerTexte End Sub
On peut encore se servir d'un code inline::
Données statiques dans un code inline
<%= ChargerTexte()%>
Ou bien utiliser un Literal et remplacer le Label par:
Données statiques chargées dans un Literal:
<asp:literal ID="Literal1" EnableViewState="false" runat="server"></asp:literal>
Code C#
protected void Page_Load(object sender, EventArgs e) { this.Literal1.Text = ChargerTexte(); }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Me.Literal1.Text = ChargerTexte End Sub
Les deux dernières solutions allègent la page d'une balise <span> en plus d'alléger le viewstate.
On pourrait également s'en sortir en créant dynamiquement le contrôle. Mais il y a une subtilité dont nous parlerons un peu plus loin.
Supposons que l'on développe un composant SuperLabel définit ainsi:
Le composant SuperLabel
Code C#
public class SuperLabel: Label { public SuperLabel() { } protected override void OnLoad(EventArgs e) { if (!this.Page.IsPostBack) { // une valeur par défaut this.Text = "Anticonstitutionnellement"; } base.OnLoad(e); } public override string Text { get { return this.ViewState["SuperLabel"] as string; } set { this.ViewState["SuperLabel"] = value; } } }
Code VB
Public Class SuperLabel Inherits Label Public Sub New() End Sub Protected Overloads Overrides Sub OnLoad(ByVal e As EventArgs) If Not Me.Page.IsPostBack Then ' Une valeur par défaut Me.Text = "Anticonstitutionnellement" End If MyBase.OnLoad(e) End Sub Public Overloads Overrides Property Text() As String Get Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("SuperLabel"), GetType(String)), String) End Get Set Me.ViewState("SuperLabel") = value End Set End Property End Class
Saurez vous trouver le bug qui s'est glissé dans le code en plus de la mauvaise utilisation du viewstate vous savez pourquoi maintenant?
Imaginons qu'un utilisateur du composant fasse ceci:
SuperLabel a un bug
<cc2:superlabel Text="Hello, le soleil brille" ID="SuperLabel1" runat="server"></cc2:superlabel>
On ne change rien d'autre. Voici ce qui s'affiche:

Ce n'est pas le texte attendu.
La raison est assez simple. Les valeurs déclaratives sont chargées avant l'événement Init par ASP et donc bien avant l'événement Load. Par conséquent la valeur par défaut écrase les valeurs déclaratives que l'on ne voit jamais apparaître. Ce n'est pas le comportement normalement attendu.
Comment faire? La bonne technique pour prendre en charge les valeur par défaut est la suivante:
une valeur par défaut
Code C#
public class SuperLabel: Label { public SuperLabel() { } public override string Text { get { if (this.ViewState["SuperLabel"] == null) { return "Anticonstitutionnellement"; } return this.ViewState["SuperLabel"] as string; } set { this.ViewState["SuperLabel"] = value; } } }
Code VB
Public Class SuperLabel Inherits Label Public Sub New() End Sub Public Overloads Overrides Property Text() As String Get If Me.ViewState("SuperLabel") Is Nothing Then Return "Anticonstitutionnellement" End If Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("SuperLabel"), GetType(String)), String) End Get Set Me.ViewState("SuperLabel") = value End Set End Property End Class
Cette fois tout fonctionne, le code est plus simple et cerise sur le gâteau, on ne surcharge pas inutilement le viewstate.
Normalement c'est le rôle de la méthode CreateChildControls d'alimenter la collection de contrôles de la page. On peut alors souhaiter ajouter dynamiquement un Label et donc écrire un code semblable à celui-ci:
Listing 6-19: Création d'un contrôle dynamique
Code C#
protected override void CreateChildControls() { base.CreateChildControls(); Label Label = new Label(); this.Controls.Add(Label); Label.Text = "Quand t'es rentré dans la maison du premier, tu ne savais pas vraiment à quoi elle ressemblait"; }
Code VB
Protected Overloads Overrides Sub CreateChildControls() MyBase.CreateChildControls Dim Label As Label = New Label Me.Controls.Add(Label) Label.Text = "Quand t'es rentré dans la maison du premier, tu ne savais pas vraiment à quoi elle ressemblait" End Sub
Ce code ajoute dynamiquement un label avec un message sur la page. En soi il fonctionne parfaitement. Mais une erreur de conception ne le rend pas optimal puisque un examen de la taille de __VIEWSTATE démontre que le texte est sérialisé. Puisqu' il s'agit d'une donnée statique cela est donc inutile. Comment faire?
Tout se joue lors de l'ajout du Label dans la collection ControlCollection. Cette collection est en fait un peu plus qu'une simple collection.
Chaque fois qu'un contrôle est ajouté à la collection de contrôles d'un contrôle lié hiérarchiquement à Page, alors ASP déclenche la séquence d'événements suivie normalement par tout contrôle: Init, LoadViewState, LoadPostBackData, Load. et génère aussi la valeur de ClientID, ce qui permet par la suite de relier le contrôle à un Validator.

On voit que le suivi est déjà activé une fois la propriété Text assignée. La bonne séquence est donc celle-ci:
Contrôle créé dynamiquement
Code C#
protected override void CreateChildControls() { base.CreateChildControls(); Label Label = new Label(); Label.Text = "Quand t'es rentré dans la maison du premier, tu ne savais pas vraiment à quoi elle ressemblait"; this.Controls.Add(Label); }
Code VB
Protected Overloads Overrides Sub CreateChildControls() MyBase.CreateChildControls Dim Label As Label = New Label Label.Text = "Quand t'es rentré dans la maison du premier, tu ne savais pas vraiment à quoi elle ressemblait" Me.Controls.Add(Label) End Sub
Comme le montre le dessin précédent, le seul moment où l'on peut agir avant que la méthode TrackViewState du contrôle ne soit activée, est avant l'ajout de celui-ci dans la collection.
Au début de ce chapitre nous avons réalisé une expérience montrant que la sauvegarde de l'état d'un contrôle ne dépend pas du viewstate. Mais refaisons la même expérience avec un contrôle Label cette fois:
Persistance d'un Label
<html> <body> <form id="form1" runat="server"> <asp:button ID="Button1" runat="server" Text="Button" /> <p> <asp:label EnableViewState="False" ID="Label1" runat="server" Text="Label"></asp:label> </p> <p> <asp:label ID="Label2" runat="server" Text="Label"></asp:label> </p> </form> </body> </html>
Code C#
protected void Page_Load(object sender, EventArgs e) { if (!this.IsPostBack) { this.Label1.Text = "Ici le label 1"; this.Label2.Text = "Ici le label 2"; } }
Code VB
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) If Not Me.IsPostBack Then Me.Label1.Text = "Ici le label 1" Me.Label2.Text = "Ici le label 2" End If End Sub
Deux Label identiques, si ce n'est que leur propriété EnabledViewState est différente.
Lors du premier chargement de la page:

On fait un click sur le bouton:

Comme on le voit le premier Label dont le viewstate est désactivé, perd son contenu.
Qu'est-ce qui peut bien différencier le cas d'un Label de celui d'un TextBox où tout marchait bien? Simplement le fait que les composants qui implémentent IPostBackEventHandler, comme les TextBox, gèrent eux même leur état. Les autres composants laissent le viewstate gérer leur état, c'est le cas du Label.
On a déjà vu que la propriété Style implémente une instance de IStateManager qui lui est propre. Reflector vous listera une bonne dizaine d'autres composants qui implémentent cette interface.
ASP 2.0 définie une nouvelle fonctionnalité dite d'état de contrôle[50] (control state).
L'état de contrôle est similaire au viewstate, mais sa portée est le contrôle. L'intérêt est que son activation est indépendante de celle du viewstate. Vous pouvez ainsi désactiver le viewstate d'une page sans perdre les fonctionnalités de vos contrôles. Par exemple le GridView a besoin de sauvegarder le numéro de page courante pour mettre en œuvre sa fonction de pagination.
L'état de contrôle est soumis aux même contraintes que le viewstate en ce qui concerne la sérialisation.
L'état de contrôle n'est pas activé par défaut. On doit le faire au moment de l'événement Init d'un contrôle:
Activation de l'état du contrôle
Code C#
public class MyControl : Control { protected override void OnInit(EventArgs e) { Page.RegisterRequiresControlState(this); base.OnInit(e); } }
Code VB
Public Class MyControl Inherits Control Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs) Page.RegisterRequiresControlState(Me) MyBase.OnInit(e) End Sub End Class
Une fois activée comment utilise t'on l'état de contrôle?
Exemple d'utilisation de l'état de contrôle:
Code C#
public class MonContol : WebControl { public MonContol() { } protected override void OnInit(EventArgs e) { this.Page.RegisterRequiresControlState(this); base.OnInit(e); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); if (this.Page.IsPostBack) { this.TotalPost++; } } protected override object SaveControlState() { Object BaseScs = base.SaveControlState(); return new object[] { BaseScs, (this.TotalPost == 0) ? null : this.TotalPost as object}; } private int _TotalPost = 0; public int TotalPost { get { return _TotalPost; } set { _TotalPost = value; } } protected override void LoadControlState(object state) { Object[] Scs = state as object[]; if (Scs != null) { base.LoadControlState(Scs[0]); if (Scs[1] != null) { this.TotalPost = Convert.ToInt32(Scs[1]); } } else { base.LoadControlState(null); } } protected override void RenderContents(HtmlTextWriter writer) { writer.Write(string.Format("Nombre de posts: {0}",this.TotalPost)); } }
Code VB
Public Class MonContol Inherits WebControl Public Sub New() End Sub Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs) Me.Page.RegisterRequiresControlState(Me) MyBase.OnInit(e) End Sub Protected Overloads Overrides Sub OnLoad(ByVal e As EventArgs) MyBase.OnLoad(e) If Me.Page.IsPostBack Then System.Math.Min(System.Threading.Interlocked.Increment(Me.TotalPost),Me.TotalPost-1) End If End Sub Protected Overloads Overrides Function SaveControlState() As Object Dim BaseScs As Object = MyBase.SaveControlState Return New Object() {BaseScs, Microsoft.VisualBasic.IIf((Me.TotalPost = 0),Nothing,CType(ConversionHelpers.AsWorkaround(Me.TotalPost, GetType(Object)), Object))} End Function Private _TotalPost As Integer = 0 Public Property TotalPost() As Integer Get Return _TotalPost End Get Set _TotalPost = value End Set End Property Protected Overloads Overrides Sub LoadControlState(ByVal state As Object) Dim Scs As Object() = CType(ConversionHelpers.AsWorkaround(state, GetType(Object())), Object()) If Not (Scs Is Nothing) Then MyBase.LoadControlState(Scs(0)) If Not (Scs(1) Is Nothing) Then Me.TotalPost = Convert.ToInt32(Scs(1)) End If Else MyBase.LoadControlState(Nothing) End If End Sub Protected Overloads Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) writer.Write(String.Format("Nombre de posts: {0}", Me.TotalPost)) End Sub End Class
Ce contrôle très simple comptabilise le nombre de fois où la page a été postée.
L'état de contrôle ne s'appuie pas sur un objet aussi sophistiqué que IStateManager. Il s'agit d'un simple objet de type Object. Cela signifie que l'on doit gérer à la main la volumétrie. Notez donc la façon dont on prend en charge les valeurs par défaut.
Bien que dans cet exemple précis ce n'est sans doute pas utile, j'ai prise en charge l'ajout éventuel d'information par la classe de base.
L'état de contrôle n'est pas adapté à la prise en charge d'un volume important de données.
Note:
Actuellement peu de contrôles utilisent l'état de
contrôle. Cette fonctionnalité ne concerne en fait que
les nouveaux contrôles d'ASP.
La bibliographie est plus étendue que celle réellement utilisée pour cet article. Une suite est en cours de préparation et à l'origine l'article sur le viewstate était un des chapitres.