Le ViewState en ASP.NET Le ViewState et comment l'optimiser Le ViewState est un système de maintien de la persistance des données ajouté dans le FrameWork.NET pour les pages ASP.NET. Ainsi dans chaque page ASP.NET où l'on a un formulaire, il existe un objet particulier que je vais essayer de présenter maintenant. Comment Optimiser le ViewState Le ViewState est un concept ajouté dans le FrameWork.NET afin d'améliorer les possibilités de développement dans le cas d'applications WEB imitant du Client-Serveur. En effet, les différentes méthodes possibles pour stocker de l'information entre deux états lors d'une navigation sur un Site Internet étaient : Les Variables de Sessions Les Cookies Le Stockage en Base de données Chacune de ses trois méthodes était intéressante mais possédait ses propres limites : Les cookies ne sont pas fiables, car on a aucune garantie sur la configuration du client quand à l'acceptation de ces cookies. Les variables de Session sont destinées à être temporaires et ne doivent pas perdurer de par l'utilisation du temps de la session du client. Le stockage en Base de Données est coûteux en conception et développement, car relativement compliqué à mettre en place et à maintenir. De plus, on sait que l'html est capable de stocker de l'information sans qu'elle soit forcément visible par l'utilisateur (sauf s'il consulte le code source), c'est déjà une astuce utilisée dans de nombreux développements plus ou moins complexes en ASP. On sait aussi que le FrameWork.NET utilise énormément le XML et donc la "Serialisation" XML. En combinant les deux, on obtient le ViewState. Ainsi Microsoft a créé un concept qui est le stockage de l'ensemble des informations de tous les composants contrôlés par le serveur dans un champ HIDDEN de la page en utilisant la "Serialisation" XML et surtout une classe développée pour cet usage : le StateBag. Voyons plus précisément comment cela fonctionne.
Le Fonctionnement du ViewState Son principe est simple, il conserve dans la page HTML envoyée au client l'état de chaque objet.net (contrôlé par le serveur) qui est placé sur la page en cours d'exécution. De ce fait, par défaut si on place un DropDownList sur une page et qu'on envoie la page à un client celui-ci reçoit les éléments classiques HTML de la page qu'il consulte mais aussi un élément caché (INPUT HIDDEN) qui mémorise l'état de cet objet. Reprenons l'exemple de l'article précédent pour l'interrogation d'indexing Service sous ASP.NET (ASP.NET et Index Server). Sur celui-ci nous avions simplement un DataGrid qui récupérait le résultat simple de la requête envoyée à Index Server. De ce fait ce DataGrid est en mode simplement de consultation et donc on n'a pas besoin de faire des Allers-Retours entre le Client et le Serveur. Ci dessous la vision obtenue par le client : Ainsi, lorsque l'on regarde la source HTML qui affiche cette page on observe dans le début du formulaire, juste après sa déclaration dans le Gabarit ASPX, un champ : input type="hidden" name=" VIEWSTATE"
Avec la valeur : Ce champ contient l'intégralité de l'état des objets contrôlés par le serveur placés dans la page. Donc dans notre cas, l'état complet du DataGrid placé sur la page. Si on regarde maintenant de plus près notre page dans l'état actuel (donc avec le ViewState du DataGrid activé) représente 3 067 Octets. Vous allez sans doute me dire que 3 Ko pour une page HTML, ce n'est rien, mais il faut garder à l'esprit que ce datagrid ne comporte que 3 Lignes et 4 Colonnes avec des cellules qui ne contiennent que du texte simple. Modifions maintenant un tout petit peu ce code d'origine en changeant juste la Requête SQL envoyée à Index Server. On enverra donc la requête qui recherche dans tout le Catalogue les documents contenant les termes 'Microsoft' ou 'DotNet'. Ce qui donne la Requête suivante : SELECT Rank, VPath, DocTitle, Filename, Characterization, Write FROM SCOPE('DEEP TRAVERSAL OF "/"') WHERE NOT CONTAINS(VPath, '"_vti_" OR ".config"') AND CONTAINS(Contents, '"Microsoft" OR "DotNet"') OR CONTAINS(DocTitle, '"Microsoft" OR "DotNet"') ORDER BY Rank DESC
On avait donc dans l'exemple précédent 3 valeurs dans notre résultat, maintenant on en a 855. Cette fois notre Page fait une taille de 380 706 Octets dont 239 189 Octets uniquement pour le ViewState. Or on sait bien que ce ViewState n'est d'aucune utilité puisque c'est une simple page consultative. Par cet exemple on voit toute l'importance que peut prendre le ViewState sur des projets où certaines pages ne servent qu'à visionner un état de la base à un temps T. Il faut donc dans ce cadre là désactiver le ViewState pour ce composant, pour cela il suffit de modifier un paramètre du DataGrid : EnableViewState="False" Au rechargement de cette même page avec juste cette option modifiée nous obtenons un fichier qui fait 141 690 Octets, soit plus de deux fois plus léger. Nous venons de voir un cas simple de désactivation du ViewState, mais voyons un peu plus loin comment on peut optimiser celui-ci.
Exemple d'utilisation du ViewState Cas de la Pagination d'un DataGrid Dans le cas précédent, on a expliqué un des exemples où le ViewState doit être désactivé pour ne pas générer de page trop lourdes et donc pour éviter de rendre le site trop pénible à utiliser. Mais cette fois, nous allons voir un exemple, où le ViewState doit être activé toujours avec la même page, ceci en ajoutant juste une option du DataGrid : Le Paging. Ce cas est toujours dans un mode DataGrid consultatif, il s'agit de l'utilisation de la Pagination (très pratique pour les clients finaux). Cette option permet aux utilisateurs de pouvoir avoir le résultat de la demande dans le DataGrid, mais par petit bloc et non tous les résultats en une seule page (d'où le nom de Pagination). Les modifications à apporter sont légères sur le Code de base de la page. Je vais donc vous fournir une version un peu plus construite que celle de l'article sur Indexing Service. Fichier TestIndex.aspx - Version Améliorée <%@ Page Language="vb" AutoEventWireup="false" Codebehind="TestIndex.aspx.vb" Inherits="DotNetSysInfo.TestIndex"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//FR"> <HTML> <HEAD> <title>test d'utilisation d'indexing Service</title> <meta content="visual Basic 7.0" name="code_language"> <meta content="javascript" name="vs_defaultclientscript"> <meta content="http://schemas.microsoft.com/intellisense/ie3-2nav3-0" name="vs_targetschema"> </HEAD> <body> <form id="form1" method="post" runat="server"> <table bgcolor="#eeeeee" cellpadding="6" width="100%"> <tr><td nowrap width="100%" align="middle"><font face="verdana" size="2"> <asp:label id="labeltitre" runat="server" Font-Size="15" /></font> </td></tr> </table> <br> <asp:datagrid id="mondatagrid" runat="server" CellPadding="4" BackColor="White" BorderWidth="1px" BorderStyle="None" BorderColor="#3366CC" Width="100%" AllowPaging="True" OnPageIndexChanged="MonDataGrid_Page" PageSize="20"> <SelectedItemStyle Font-Bold="True" ForeColor="#CCFF99" BackColor="#009999"></SelectedItemStyle> <ItemStyle ForeColor="#003399" BackColor="White"></ItemStyle> <HeaderStyle Font-Bold="True" ForeColor="#CCCCFF" BackColor="#003399"></HeaderStyle> <FooterStyle ForeColor="#003399" BackColor="#99CCCC"></FooterStyle> <PagerStyle HorizontalAlign="Center" ForeColor="#003399" Position="TopAndBottom" BackColor="#99CCCC" Mode="NumericPages"></PagerStyle> </asp:datagrid> <p> <table bgcolor="#eeeeee" cellpadding="6" width="100%"> <tr><td width="100%"><font face="verdana" size="-2"> <asp:label id="labelrequête" runat="server" /></font> </td></tr> <tr><td nowrap width="100%"><font face="verdana" size="-2"> <asp:label id="lblcurrentindex" runat="server" /><br> <asp:label id="lblpagecount" runat="server" /><br> <asp:label id="labelnbdatagrid" runat="server" /><br> </font> </td></tr> </table> </p> </form> </body> </HTML>
Fichier TestIndex.aspx.vb - Version Améliorée ' Exemple simple de recherche dans un catalogue Index Services EN vb.net Imports System.Data Imports System.Data.OleDb Imports System.Collections Imports System.Collections.Specialized Public Class TestIndex Inherits System.Web.UI.Page Protected WithEvents MonDataGrid As System.Web.UI.WebControls.DataGrid Protected WithEvents LabelRequête As System.Web.UI.WebControls.Label Protected WithEvents lblcurrentindex As System.Web.UI.WebControls.Label Protected WithEvents lblpagecount As System.Web.UI.WebControls.Label Protected WithEvents LabelNbDataGrid As System.Web.UI.WebControls.Label Protected WithEvents LabelTitre As System.Web.UI.WebControls.Label Private MonoleDbSelectCommand1 As New System.Data.OleDb.OleDbCommand() Private MondbAdapter As New System.Data.OleDb.OleDbDataAdapter() Private MadbConnection As New System.Data.OleDb.OleDbConnection() Private MaDataTable As New DataTable() Private LaRequête As String = "" #Region " Code généré par le Concepteur Web Form " 'Cet appel est requis par le Concepteur Web Form. <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Init 'CODEGEN : cet appel de méthode est requis par le Concepteur Web Form 'Ne le modifiez pas en utilisant l'éditeur de code. InitializeComponent() #End Region Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load ' Chargement de la Page ChargeDataGrig() Sub MonDataGrid_Page(ByVal sender As Object, ByVal e As DataGridPageChangedEventArgs) ' Gestion simple du Paging MonDataGrid.CurrentPageIndex = e.newpageindex ChargeDataGrig() Private Sub chargedatatable() Me.MondbAdapter.SelectCommand = Me.MonoleDbSelectCommand1 Me.MonoleDbSelectCommand1.Connection = Me.MadbConnection ' Initialisation de la connexion avec notre catalogue Me.MadbConnection.ConnectionString = "Provider=MSIDXS;Data Source=DotNetQueDuBonheur" ' Création de la requête pour ADO.NET avec la recherche du mot Machine et DotNet LaRequête = "SELECT Rank, VPath, DocTitle, Filename, Characterization, Write " & _ "FROM SCOPE('DEEP TRAVERSAL OF ""/""') " & _ "WHERE NOT CONTAINS(VPath, '""_vti_"" OR "".config""') " & _ "AND CONTAINS(Contents, '""Microsoft"" OR ""DotNet""') " & _ "OR CONTAINS(DocTitle, '""Microsoft"" OR ""DotNet""') " & _ "ORDER BY Rank DESC" LabelRequête.Text = LaRequête ' Assigne la requête et charge le résultat Me.MondbAdapter.SelectCommand.CommandText = LaRequête Me.MondbAdapter.Fill(MaDataTable) Private Sub ChargeDataGrig() 'Charge les Données dans le DataTable chargedatatable() ' Envoie le résultat dans le Datagrid Me.MonDataGrid.DataSource = MaDataTable Me.MonDataGrid.DataBind() ShowStats() Sub ShowStats() 'Affichage des Statistiques sur la DataTable lblcurrentindex.text = "CurrentPageIndex :" & MonDataGrid.CurrentPageIndex lblpagecount.text = "PageCount : " & MonDataGrid.PageCount LabelNbDataGrid.Text = "Nombre de Valeurs dans le Datagrid : " & MaDataTable.Rows.Count LabelTitre.Text = "Chargement de la Recherche des Document dans Index Server" End Class
La gestion de la Pagination est assurée par la Procédure MonDataGrid_Page. On obtient alors la capture d'écran ci-dessous : On obtient alors un ViewState contenant toutes les informations du DataGrid mais aussi de tous les Labels placés dans cette page. On a donc une page qui fait un poids de 10 558 Octets.
Ceci est bien plus léger que l'affichage précédent de toutes les valeurs, mais on peut encore réduire ce ViewState, Il suffit pour cela de déclarer l'activation du ViewState à False pour tous les Labels de cette page. Une fois cette désactivation faite on a une page qui fait 9 778 Octets. On ne peut pas toucher à la sauvegarde du ViewState pour le DataGrid dans cette méthode de Pagination, il en existe d'autres qui généreront un ViewState plus petit, mais le But de cet exemple est de vous montrer l'intérêt de bien faire attention à ce qui est nécessaire à sauvegarder dans le ViewState. Si vous souhaitez un exemple de Pagination Optimisée pour un ViewState très Light, allez sur le site de Richard Clark (C2i.fr) : Un DataGrid paginé optimisé (ie sans ViewState) Maintenant, voyons un dernier exemple plus abouti toujour utilisant Indexing Service.
Utilisation d'index Server avec la Pagination et Optimisation du ViewState Maintenant dans ce dernier exemple, nous allons conserver l'utilisation du DataGrid Paginé, mais rajouter l'option de recherche plus pointue et dynamique dans Index Server. On sait par le tutorial précédent que l'on peut bloquer la recherche dans le Catalogue à un sous répertoire d'iis. On va donc utiliser une lecture du répertoire racine afin de les proposer dans une DropDownList. Il faut déjà ajouter quelques Clés dans le Web.Config :... <appsettings> <add key="catalogueindexserver" value="dotnetquedubonheur" /> <add key="repertoireracineiis" value="e:\sites_web\page-perso" /> <add key="cnxindexserver" value="provider=msidxs;data Source=DotNetQueDuBonheur"/>... </appsettings>...
Maintenant voyons le Code lui même : Fichier TestIndex.aspx - Version Finale <%@ Page Language="vb" AutoEventWireup="false" Codebehind="TestIndex.aspx.vb" Inherits="DotNetSysInfo.TestIndex"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//FR"> <HTML> <HEAD> <title>test d'utilisation d'indexing Service</title> <meta content="visual Basic 7.0" name="code_language"> <meta content="javascript" name="vs_defaultclientscript"> <meta content="http://schemas.microsoft.com/intellisense/ie3-2nav3-0" name="vs_targetschema"> </HEAD> <body> <form id="form1" method="post" runat="server"> <table cellpadding="6" width="100%" bgcolor="#eeeeee"> <tr> <td nowrap align="middle" width="100%"><font face="verdana" size="2"> <asp:label id="labeltitre" runat="server" Font-Size="15" EnableViewState="False" /></font></td> </tr> </table> <br> <table cellpadding="6" width="100%" bgcolor="#eeeeee"> <tr> Premier Mot Recherché dans le catalogue :</font></td> <asp:textbox id="motcatalogue" runat="server" Width="300"/></font></td> </tr><tr> Second Mot Recherché dans le catalogue :</font></td> <asp:textbox id="motcatalogue2" runat="server" Width="300" /></font></td> </tr><tr> Catalogue Interrogé :</font></td> <asp:textbox id="catalogue" runat="server" Width="300" Enabled="False" /></font></td> </tr><tr> Limitation au Sous Répertoire :</font></td> <asp:dropdownlist id="dropdownsousrepertoire" runat="server" Width="300" /></font></td> </tr><tr> <td width="100%" colspan="2" align="middle"> <asp:button id="btnmemo" runat="server" Width="200px" Text="Lancer la Recherche" /> </td> </tr> </table> <br> <asp:datagrid id="mondatagrid" runat="server" AllowPaging="True" Width="100%" BorderColor="#3366CC" BorderStyle="None" BorderWidth="1px" BackColor="White" CellPadding="4" OnPageIndexChanged="MonDataGrid_Page" PageSize="10"> <SelectedItemStyle Font-Bold="True" ForeColor="#CCFF99" BackColor="#009999" /> <ItemStyle ForeColor="#003399" BackColor="White" /> <HeaderStyle Font-Bold="True" ForeColor="#CCCCFF" BackColor="#003399" /> <FooterStyle ForeColor="#003399" BackColor="#99CCCC" /> <PagerStyle HorizontalAlign="Center" ForeColor="#003399" Position="TopAndBottom" BackColor="#99CCCC" Mode="NumericPages" /> </asp:datagrid> <p> <table cellpadding="6" width="100%" bgcolor="#eeeeee"> <tr> <td width="100%"><font face="verdana" size="-2"> <asp:label id="labelrequête" runat="server" EnableViewState="False" /></font> </td> </tr> <tr> <td nowrap width="100%"><font face="verdana" size="-2"> <asp:label id="lblcurrentindex" runat="server" EnableViewState="False" /><br> <asp:label id="lblpagecount" runat="server" EnableViewState="False" /><br> <asp:label id="labelnbdatagrid" runat="server" EnableViewState="False" /><br> </font> </td> </tr> </table> </p> </form></body></html>
Fichier TestIndex.aspx.vb - Version Finale ' Exemple simple de recherche dans un catalogue Index Services EN vb.net Imports System.Data Imports System.Data.OleDb Imports System.Collections Imports System.Collections.Specialized Public Class TestIndex Inherits System.Web.UI.Page Protected WithEvents MonDataGrid As System.Web.UI.WebControls.DataGrid Protected WithEvents LabelRequête As System.Web.UI.WebControls.Label Protected WithEvents lblcurrentindex As System.Web.UI.WebControls.Label Protected WithEvents lblpagecount As System.Web.UI.WebControls.Label Protected WithEvents LabelNbDataGrid As System.Web.UI.WebControls.Label Protected WithEvents LabelTitre As System.Web.UI.WebControls.Label Protected WithEvents MotCatalogue As System.Web.UI.WebControls.TextBox Protected WithEvents MotCatalogue2 As System.Web.UI.WebControls.TextBox Protected WithEvents Catalogue As System.Web.UI.WebControls.TextBox Protected WithEvents DropDownSousRepertoire As System.Web.UI.WebControls.DropDownList Protected WithEvents btnmemo As System.Web.UI.WebControls.Button Private MonoleDbSelectCommand1 As New System.Data.OleDb.OleDbCommand() Private MondbAdapter As New System.Data.OleDb.OleDbDataAdapter() Private MadbConnection As New System.Data.OleDb.OleDbConnection() Private MaDataTable As New DataTable() Private LaRequête As String = "" #Region " Code généré par le Concepteur Web Form " 'Cet appel est requis par le Concepteur Web Form. <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Init 'CODEGEN : cet appel de méthode est requis par le Concepteur Web Form 'Ne le modifiez pas en utilisant l'éditeur de code. InitializeComponent() #End Region Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles MyBase.Load ' Chargement de la Page Dim LaTable As New System.Data.DataTable() Catalogue.Text = ConfigurationSettings.AppSettings("CatalogueIndexServer") If Not Page.IsPostBack Then LaTable = ChargeListeSousRep(ConfigurationSettings.AppSettings("RepertoireRacineIIS")) DropDownSousRepertoire.DataSource = LaTable DropDownSousRepertoire.DataTextField = "Nom" DropDownSousRepertoire.DataValueField = "Chemin" DropDownSousRepertoire.DataBind() DropDownSousRepertoire.Visible = True End If Sub MonDataGrid_Page(ByVal sender As Object, ByVal e As DataGridPageChangedEventArgs) ' Gestion simple du Paging MonDataGrid.CurrentPageIndex = e.newpageindex ChargeDataGrig() Private Sub chargedatatable() Me.MondbAdapter.SelectCommand = Me.MonoleDbSelectCommand1 Me.MonoleDbSelectCommand1.Connection = Me.MadbConnection ' Initialisation de la connexion avec notre catalogue Me.MadbConnection.ConnectionString = ConfigurationSettings.AppSettings("CnxIndexServer") ' Création de la requête pour ADO.NET avec la recherche du mot Machine et DotNet LaRequête = "SELECT Rank, VPath, DocTitle, Filename, Characterization, Write " & _ "FROM SCOPE('DEEP TRAVERSAL " & _
"OF """ & Trim(DropDownSousRepertoire.SelectedItem.Value) & """') " & _ "WHERE NOT CONTAINS(VPath, '""_vti_"" " & _ "OR "".config""') " & _ "AND CONTAINS(Contents, '""" & Trim(MotCatalogue.Text) & """ " & _ "OR """ & Trim(MotCatalogue2.Text) & """') " & _ "OR CONTAINS(DocTitle, '""" & Trim(MotCatalogue.Text) & """ " & _ "OR """ & Trim(MotCatalogue2.Text) & """') " & _ "ORDER BY Rank DESC" LabelRequête.Text = LaRequête ' Assigne la requête et charge le résultat Me.MondbAdapter.SelectCommand.CommandText = LaRequête Me.MondbAdapter.Fill(MaDataTable) Private Sub ChargeDataGrig() 'Charge les Données dans le DataTable chargedatatable() ' Envoie le résultat dans le Datagrid Me.MonDataGrid.DataSource = MaDataTable Me.MonDataGrid.DataBind() ShowStats() Sub ShowStats() 'Affichage des Statistiques sur la DataTable lblcurrentindex.text = "CurrentPageIndex :" & MonDataGrid.CurrentPageIndex lblpagecount.text = "PageCount : " & MonDataGrid.PageCount LabelNbDataGrid.Text = "Nombre de Valeurs dans le Datagrid : " & MaDataTable.Rows.Count LabelTitre.Text = "Chargement de la Recherche des Document dans Index Server" Private Function ChargeListeSousRep(ByVal Racine As String) As DataTable Dim LaTableTemp As New System.Data.DataTable() Dim mycolumn As DataColumn = New DataColumn() Dim myrow As DataRow ' Paramétrage des Colonnes mycolumn.datatype = System.Type.GetType("System.String") mycolumn.allowdbnull = False mycolumn.caption = "Chemin" mycolumn.columnname = "Chemin" LaTableTemp.Columns.Add(myColumn) mycolumn = New DataColumn() mycolumn.datatype = System.Type.GetType("System.String") mycolumn.caption = "Nom" mycolumn.columnname = "Nom" LaTableTemp.Columns.Add(myColumn) myrow = LaTableTemp.NewRow() myrow("chemin") = "" myrow("nom") = "\" LaTableTemp.Rows.Add(myRow) Dim ListeSousRepertoires As String() = System.IO.Directory.GetDirectories(Racine) Dim PosFinale As Integer = Racine.Length Dim subdirectory As String For Each subdirectory In ListeSousRepertoires myrow = LaTableTemp.NewRow() myrow("chemin") = subdirectory.substring(posfinale + 1) myrow("nom") = subdirectory.substring(posfinale) LaTableTemp.Rows.Add(myRow) Next subdirectory Return LaTableTemp End Function ' ----------------------------------------------------- Private Sub btnmemo_click(byval sender As System.Object, ByVal e As System.EventArgs) _ Handles btnmemo.click ChargeDataGrig() ' ----------------------------------------------------- End Class
L'exécution de cette page donne alors le résultat suivant :
Conclusion Cet article a pour but de présenter simplement le ViewState d'asp.net afin d'attirer l'attention sur l'obligation d'optimiser celui-ci lors du codage de vos pages dynamiques afin d'optimiser la taille du code HTML résultant de cette exécution. Si vous souhaitez en savoir plus sur cet option d'asp.net, vous pouvez aller sur ces quelques adresses dédiées : Didacticiel Microsoft - Gestion de l'état de l'application (FR) W3 Schools - ASP.NET Maintaining the ViewState (US) Code Project - Help! My ViewState Is Out Of Control (US) DotNet Bips - Using Server.Transfer and EnableViewStateMac to Preserve Form State (US) Web Reference - Viewstate Optimization Strategies in ASP.NET (US) En vous souhaitant de bons projets de développement. Romelard Fabrice (alias F )