Le Gray's Anatomy du viewstate

   Auteur : Frédéric De Lène Mirouze (amethyste16@hotmail.com)

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

Introduction

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?

Comprendre le viewstate

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.
Vous pouvez pourtant constater qu'une saisie dans le TextBox résiste à un renvoi. Vous pouvez même constater que le contenu du champ caché __VIEWSTATE ne change pas quel que soit le contenu de ladite saisie.

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:

  1. Enregistrer des valeurs pour chaque contrôle dans une table de hachage
  2. Détecter les modifications de la valeur initiale du viewstate
  3. Sérialiser et désérialiser
  4. Restaurer automatiquement les données

Enregistrer des valeurs

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#

  • bool IsTrackingViewState
  • void LoadViewState(object state)
  • object SaveViewState()
  • void TrackViewState()

Code VB

  • ReadOnly Property IsTrackingViewState As Boolean
  • Sub LoadViewState(ByVal state As Object)
  • Function SaveViewState() As Object
  • Sub TrackViewState()

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.

Détecter les modifications dans ViewState

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:

 

Sérialiser et désérialiser des données dans un champ caché de la page

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.

Restaurer automatiquement les données

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].

Optimiser la taille du viewstate

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:

  1. on lance la page et note la valeur de __VIEWSTATE
  2. on clique sur le bouton et note la valeur de __VIEWSTATE

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.

Persistance de données statiques

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.

Les valeurs par défaut

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.

Initialiser les contrôles créés dynamiquement

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.

Viewstate ne gère pas l'état, vraiment?

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>&nbsp;</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.

D'autres viewstate

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.

Bibliographie

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. 

  1. Merci à Manoli pour son formateur de code en HTML:
    http://www.manoli.net/csharpformat/
  2. Article de Bertrand leroy sur les attaques par injection de code
    http://weblogs.asp.net/bleroy/archive/2004/08/18/216861.aspx
  3. Articles de Scott Allen sur la validation des événements:
    http://odetocode.com/Blogs/scott/archive/2006/03/20/3145.aspx
    http://odetocode.com/Blogs/scott/archive/2006/03/21/3153.aspx
  4. Developing Microsoft ASP.NET server controls and components
    Microsoft press
    ISBN: 0-7356-1582-9
  5. Building ASP.NET server controls
    Apress
    Rob Cameron, Dale Michalk
  6. Article de Paul Wilson sur le format du viewstate avec le source d'un analyseur et à quoi sert le viewstate:
    http://aspalliance.com/articleViewer.aspx?aId=135&pId=
  7. Analyseur de ViewState:
    http://sharptoolbox.com/tools/page-viewstate-parser
  8. Comment obtenir le Button qui a causé un renvoi:
    http://www.ryanfarley.com/blog/archive/2005/03/11/1886.aspx
  9. Article de Xefteri sur le postback:
    http://www.xefteri.com/articles/show.cfm?id=18
  10. Fonctionnement de doPostBack:
    http://aspalliance.com/895
  11. Un convertisseur de code VB/C# et inversement qui m'a fait gagner pas mal de temps:
    http://www.developerfusion.co.uk/utilities/convertcsharptovb.aspx
  12. Modèle d'événement des contrôles:
    http://msdn2.microsoft.com/en-us/library/y3bwdsh3.aspx
  13. Tout savoir sur le viewstate:
    http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx
  14. Le cycle des événements:
    http://www.15seconds.com/issue/020102.htm
  15. Vous croyez comprendre quelque chose au viewstate? Ben voyons!
    http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx
  16. Est t'il seulement possible de faire du .NET sans l'outil Reflector?
    http://www.aisto.com/roeder/dotnet/
  17. Sur la signature des délégués d'événement:
    http://www.dotnetguru2.org/index.php?p=92&more=1&c=1&tb=1&pb=1
  18. Syntaxe des événements:
    http://www.panopticoncentral.net/archive/2004/08/03/1536.aspx
  19. Les ressources incluses:
    http://www.dotnetguru2.org/index.php?p=506&more=1&c=1&tb=1&pb=1
  20. Tout sur WebForm_DoPostBackWithOptions():
    http://www.carlosag.net/Articles/WebParts/whidbeyfaq.aspx
  21. Fonctionnement du renvoi inter page dans un article dédié à la persistance des informations utilisateur:
    http://www.codeproject.com/books/ASPNET20.asp
  22. Incompatibilités de la validation et certains navigateurs:
    http://aspnet.4guysfromrolla.com/articles/051204-1.aspx
  23. Bouton cliquable une fois:
    http://blog.shkedy.com/
  24. Pourquoi runat=server sur un contrôle HTML?
    http://blogs.msdn.com/dancre/archive/2006/12/17/what-does-runat-server-do-for-html-controls.aspx
  25. Cycle de vie d'une page ASP:
    http://msdn2.microsoft.com/en-us/library/ms178472(VS.80).aspx
  26. Le modèle d'événements des contrôles ASP:
    http://msdn2.microsoft.com/en-us/library/y3bwdsh3(VS.80).aspx
  27. Implémenter un PageStatePersister:
    http://msdn2.microsoft.com/en-us/library/system.web.ui.pagestatepersister(VS.80).aspx#
  28. Aperçu du viewstate
    http://msdn2.microsoft.com/en-us/library/y3bwdsh3(VS.80).aspx
  29. Les coulisses d'ASP
    http://msdn2.microsoft.com/en-us/library/ms379581(VS.80).aspx
  30. Sur le cycle de vie d'ASP:
    http://msdn2.microsoft.com/en-us/library/ms227435(vs.80).aspx
  31. Le modèle objet d'ASP:
    http://msdn2.microsoft.com/en-us/library/aa479007.aspx
  32. Au sujet du message: control XXXX must be placed inside a form tag with runat=server
    http://aspnet.4guysfromrolla.com/demos/printPage.aspx?path=/articles/102203-1.aspx
  33. Création dynamique de contrôles:
    http://www.firoz.name/2006/05/28/working-with-dynamic-controls-basics/
  34. Sur la nouvelle version de __doPostback en ASP 2.0:
    http://weblogs.asp.net/vga/archive/2004/03/01/NoMoreHijackingOfDoPostBackInWhidbey.aspx
  35. Comparaison entre DataGrid et GridView:
    http://msdn2.microsoft.com/fr-fr/library/05yye6k9(VS.80).aspx
  36. Une série d'articles sur les contrôles dynamiques:
    http://scottonwriting.net/sowblog/posts/3962.aspx
    http://msdn2.microsoft.com/en-us/library/aa479330.aspx
    http://aspnet.4guysfromrolla.com/articles/081402-1.aspx
    http://aspnet.4guysfromrolla.com/articles/082102-1.aspx
    http://aspnet.4guysfromrolla.com/articles/092904-1.aspx
    http://www.tutorialized.com/tutorial/Dynamic-Controls-with-Events/2755
  37. La hiérarchie des contrôles:
    http://msdn2.microsoft.com/en-us/library/aa479330.aspx
  38. Un PlaceHolder qui persiste ses contrôles enfants:
    http://www.denisbauer.com/ASPNETControls/DynamicControlsPlaceholder.aspx
  39. Discussion sur la nécessité d'implémenter IPostBackDataHandler dans certains cas:
    http://west-wind.com/weblog/posts/6666.aspx
  40. IPostBackDataHandler et Viewstate
    http://www.codeproject.com/aspnet/ViewState.asp
  41. Comment passer un événement à travers la hiérarchie des composants
    http://aspnet.4guysfromrolla.com/articles/051105-1.aspx
  42. Tutoriel sur les GridView:
    http://aspnet.4guysfromrolla.com/articles/040502-1.aspx
  43. Visualisation du contenu d'un viewstate:
    http://www.develop.com/us/technology/resourcedetail.aspx?id=d8f9ba3c-c75c-4bed-8596-55e7434d8ecd
  44. Implémenter un PageStatePersister:
    http://aspnet.4guysfromrolla.com/articles/011707-1.aspx
  45. Validation d'une chaîne de requête:
    http://msdn.microsoft.com/msdnmag/issues/07/03/CuttingEdge/default.aspx?loc=fr
  46. IPostBackEventHandler et IPostbackDataHandler en ASP 2.0
    http://aspadvice.com/blogs/joteke/archive/2005/09/26/12894.aspx
  47. Les membres de l'objet Page:
    http://msdn2.microsoft.com/fr-fr/library/system.web.ui.page_members(VS.80).aspx
  48. Ordonnancement des événements:
    http://weblogs.asp.net/jeff/archive/2004/07/04/172683.aspx
  49. Les pages maîtres, leurs coulisses:
    http://www.codeproject.com/aspnet/InsideMasterPages.asp?df=100&forumid=208804&exp=0&select=1339977
  50. L'état du contrôle:
    http://fredrik.nsquared2.com/viewpost.aspx?PostID=265