Cet article est une introduction au contrôle UpdatePanel disponible avec le Framework ASP.NET AJAX. Sa facilité d’utilisation et les bénéfices qu’il apporte à nos applications web en termes d’interface utilisateur le rendent incontournable ! Cependant, il est important d’apprendre à mieux le connaître afin d’optimiser son utilisation et profiter de ses caractéristiques pour aller encore plus loin dans l’amélioration des performances de nos applications web.
Après un rappel des bases de l’UpdatePanel, de son comportement par défaut et de ses propriétés à connaître, nous verrons la mise en application des notions abordées à l’aide d’un exemple de vue maître/détail d’une liste d’employés. Nous partirons ensuite de cet exemple pour aller encore plus loin dans l’utilisation de l’UpdatePanel.
La solution d’exemple proposée a été réalisée avec Visual Studio 2008 et utilise une base de données incluse dans les sources de l’exemple. Il vous faudra un SQL Express ou un SQL Server standard pour pouvoir exploiter cette dernière.
L’outil Fiddler est un outil de trace HTTP gratuit et très utile pour analyser le trafic entre votre serveur et votre navigateur client.
Figure 1 - Capture d'écran de la section AJAX Extensions de la Toolbox de VS 2008
L’Update Panel fait partie des extensions AJAX. Il permet de rafraichir des portions de pages au lieu de rafraîchir l’ensemble de la page à chaque aller-retour serveur (postback) ; et ceci sans avoir à écrire la moindre ligne de JavaScript.
On appelle ce rafraîchissement un « rendu partiel » de la page.
Le processus de rafraîchissement est coordonné par un contrôle ScriptManager qu’il suffit d’ajouter à chaque page ou directement dans la master page de votre site.
Par défaut, l’ensemble des contrôles contenus à l’intérieur d’un UpdatePanel et qui provoquent un postback classique en tant normal déclenchent alors un appel asynchrone au serveur.
Point très important : côté serveur, un appel asynchrone se comporte comme un postback classique et la page va passer par son cycle de vie habituel (Init, Load, Render) ; et ainsi toute la page est exécutée.
Cependant, alors que sur un postback classique l’ensemble du code HTML de la page est envoyé au navigateur client, dans le cas d’un appel asynchrone, seul le code HTML associé aux portions à rafraîchir va être envoyé. Charge ensuite au code JavaScript envoyé automatiquement par le ScriptManager au navigateur client de manipuler le DOM (Document Object Model) de la page afin de mettre à jour telle ou telle partie de l’arbre de contrôles affiché à l’utilisateur (pour information, cette manipulation se fait au travers de l’objet PageRequestManager).
Voilà ce que donne un glisser/déposer du contrôle UpdatePanel dans une page auquel nous avons ajouté un contrôle de type Button:
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Button ID="Button1" runat="server" Text="Button" />
</ContentTemplate>
</asp:UpdatePanel>
Le contenu de notre UpdatePanel est imbriqué dans un template « ContentTemplate ». C’est le contenu de ce ContentTemplate qui sera renvoyé au client lors d’un rafraîchissement de cet UpdatePanel.
Toujours dans notre exemple, un clic sur Button1 va déclencher un appel asynchrone au serveur. Cet appel asynchrone va déclencher l’instanciation de la page et l’exécution du code associé. Et une fois la page rendue en HTML, la portion de code correspondant au rendu du contenu du ContentTemplate (ici le rendu HTML de Button1) va être envoyé au client et rafraîchie.
Ainsi, pour une page contenant le code suivant (on notera l’ajout indispensable d’un contrôle de type ScriptManager) :
<asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Button ID="Button1" runat="server" Text="Button" />
</ContentTemplate>
</asp:UpdatePanel>
Un premier appel à cette page nous retourne le code HTML résultant de l’exécution de toute la page.
Ce code contient entre autres :
1. Une « script resource » contenant le code JavaScript d’un fichier appelé MicrosoftAjax.js
<script src="/UpdatePanel/ScriptResource.axd?d=RUh6Eldv26J_Y68_uz3iDbpY367R6e_PqNTwLkGYO17S3nzktkObWIHCEpOIc5EG-If2DMkXvXIwA2Pz9iLJUoJic3EDu_6SNxWHnlGVjUo1&t=3cd84e1b" type="text/javascript"></script>
2. Une « script resource » contenant le code JavaScript d’un fichier appelé MicrosoftAjaxWebForms.js
<script src="/UpdatePanel/ScriptResource.axd?d=RUh6Eldv26J_Y68_uz3iDbpY367R6e_PqNTwLkGYO17S3nzktkObWIHCEpOIc5EGt8KFXDDkTIUaIIJhVS73w6kQnPcjnOVaxJQa2X0jdjo1&t=3cd84e1b" type="text/javascript"></script>
Ces deux ressources JavaScript ont été en fait ajoutées automatiquement par le ScriptManager. Elles font partie de ce qu’on appelle la Microsoft Ajax Library, autrement dit l’API JavaScript d’ASP.NET AJAX.
3. Le code HTML du rendu de Button1 :
<input type="submit" name="Button1" value="Button" id="Button1" />
Lors du clic sur le bouton, on effectue alors un POST du formulaire associé à la page ; et comme ce bouton est contenu dans notre UpdatePanel, ce POST se fait de manière asynchrone.
Une trace de ce POST via l’outil Fiddler nous donne alors le résultat suivant :
POST /UpdatePanel/Default.aspx HTTP/1.1
Accept: */*
Accept-Language: fr-FR,en-US;q=0.5
Referer: http://jt/UpdatePanel/Default.aspx
x-microsoftajax: Delta=true
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Cache-Control: no-cache
UA-CPU: x86
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.5.21022; MS-RTC LM 8; WWTClient2; .NET CLR 3.5.30729; .NET CLR 3.0.30618)
Host: jt
Content-Length: 245
Proxy-Connection: Keep-Alive
Pragma: no-cache
ScriptManager1=UpdatePanel1%7CButton1&__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=%2FwEPDwUKMTk4MjkyMjU5NmRkCF1EWAliz4C%2FjlDPp2GZXve%2BLNs%3D&__EVENTVALIDATION=%2FwEWAgKUldHaAQKM54rGBoc%2FpxHq465ika9KyfQD4OJvYiLs&__ASYNCPOST=true&Button1=Button
On remarque que lors de ce POST asynchrone, la source du POST et le contenu du VIEWSTATE sont envoyés au serveur. Autrement dit, le serveur aura toutes les informations nécessaires pour procéder au traitement de la requête comme s’il s’agissait d’un postback classique.
Ensuite, un passage rapide dans l’outil de debug de Visual Studio 2008 nous confirme que la page est exécutée normalement … mais une fois la page exécutée en mémoire, seul le code HTML suivant est envoyé au client :
90|updatePanel|UpdatePanel1|
<input type="submit" name="Button1" value="Button" id="Button1" />
|52|hiddenField|__VIEWSTATE|/wEPDwUKMTk4MjkyMjU5NmRkCF1EWAliz4C/jlDPp2GZXve+LNs=|48|hiddenField|__EVENTVALIDATION|/wEWAgKUldHaAQKM54rGBoc/pxHq465ika9KyfQD4OJvYiLs|0|asyncPostBackControlIDs|||0|postBackControlIDs|||13|updatePanelIDs||tUpdatePanel1|0|childUpdatePanelIDs|||12|panelsToRefreshIDs||UpdatePanel1|2|asyncPostBackTimeout||90|12|formAction||Default.aspx|12|pageTitle||Update Panel|
On peut constater que le contenu de notre UpdatePanel est renvoyé, ainsi que le contenu du VIEWSTATE (éventuellement modifié côté serveur) mais pas le rendu HTML de la page entière ! On note au passage que le contenu renvoyé est encodé en JSON.
Par défaut, à chaque appel asynchrone sur notre page, l’ensemble des contenus des UpdatePanels vont être mis à jour. Pour comprendre ce fonctionnement par défaut, il est nécessaire de connaître certaines des propriétés de l’UpdatePanel.
Cette propriété détermine le mode de rafraîchissement de l’UpdatePanel. Par défaut, sa valeur est à Always : à chaque appel le contenu de l’UpdatePanel sera mis à jour. Lorsque l’UpdateMode est à la valeur Conditional, les choses se compliquent un peu :
· Si la propriété ChildrenAsTriggers est à true (valeur par défaut), l’UpdatePanel est mis à jour si un de ses contrôles enfants provoque un retour serveur
· L’UpdatePanel peut être piloté depuis le code et un appel à sa méthode Update() provoque son rafraîchissement.
Ainsi, si un UpdatePanel a sa propriété ChildrenAsTriggers à false et son UpdateMode à Conditional, son contenu ne sera pas rafraichi (à moins d’appeler sa méthode Update() ou bien d’avoir ajouté des triggers de façon déclarative dans le code, ce que nous verrons un peu plus loin dans l’article).
Comme indiqué ci-dessus ; à true (valeur par défaut) l’UpdatePanel se rafraichi lorsqu’un contrôle enfant provoque un retour serveur. A false, l’UpdatePanel n’est rafraichi que lors d’un appel à la méthode Update() ou bien si un trigger a été déclaré dans le code de la page aspx.
Afin d’être retrouvé et manipulé dans le DOM, l’UpdatePanel a un rendu HTML donné. Ce rendu est par défaut un élément DIV qui correspond au RenderMode Block. S’il est à la valeur Inline, l’UpdatePanel est rendu sous la forme d’un SPAN. Ainsi si vous ajoutez à posteriori vos UpdatePanel à votre application un RenderMode sur la valeur Block aura potentiellement plus d’incidence sur votre rendu graphique ; un élément DIV provoquant par défaut un retour à la ligne avant et après son contenu.
Des « triggers » peuvent être ajoutés sur chaque UpdatePanel. Ces triggers permettent de spécifier quels contrôles (enfants ou extérieurs) vont déclencher le rafraichissement de l’UpdatePanel.
Exemple :
<asp:UpdatePanel ID="UpdatePanel2" UpdateMode="Conditional" runat="server">
<ContentTemplate>
<asp:Button ID="Button2" runat="server" Text="Button" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" EventName="Click" />
</Triggers>
</asp:UpdatePanel>
UpdatePanel2 est alors mis à jour lorsque l’utilisateur clique sur Button1. La propriété EventName n’est pas indispensable dans notre cas, sa valeur par défaut étant l’événement par défaut du contrôle cible ; ici, l’événement Click. Evidemment, ces triggers n’ont d’intérêt que lorsque la propriété UpdateMode est à la valeur Conditional.
Les bases acquises, commençons par un petit exemple simple. La solution proposée en téléchargement est réalisée sous Visual Studio 2008. Cette solution utilise une petite base de données (qui n’est que la base exemple de SQL Server AdventureWorks … après une bonne cure d’amaigrissement). Il vous faut remonter le backup de cette base et configurer en conséquence la chaîne de connexion de l’application.
Cet exemple, va consister à mettre en place une vue maître/détail classique. On affichera donc une liste d’employés (table HumanResources.Employee, on ne prendra que les 20 premiers). Un clic sur un employé permettra d’afficher son adresse dans une popup modale (table Person.Address).
La démarche est la suivante : on commence sans AJAX … et on « AJAXifie » une fois tout en place ; en prenant en compte les nouvelles contraintes apportées par cette « AJAXification » (désolé pour ces néologismes barbares). C’est cette approche que je vous conseille dans la réalisation de projet utilisant les UpdatePanels.
Il ne s’agit pas de montrer les Best Practices en terme d’architecture … je ferai simple et rapide. La solution est basée sur l’utilisation de LinqToSQL, de Repeaters et une pincée de CSS.
Avant tout, on crée un nouveau projet de type Web Application.
On commence par ajouter un fichier .dbml à notre projet web.
Un glisser/déposer de toutes les tables de notre base de données et le tour est joué (comme je disais … on fait simple).
La récupération des 20 premiers employés consiste alors à faire ceci :
DataClassesDataContext dc = new DataClassesDataContext();
dc.Employees.OrderBy(emp => emp.Contact.LastName).Take(20);
Affichage de la liste des employés
Afin d’avoir la main sur le contenu généré et être en mesure d’optimiser son code, j’opte très souvent pour un contrôle Repeater plutôt que des contrôles plus complexes utilisés à 10% … C’est d’autant plus intéressant si un travail d’intégration graphique est nécessaire.
Cela donne donc ça dans le code de notre page aspx :
<h1>Liste des 20 premiers employés :</h1>
<ol>
<asp:Repeater ID="repEmployees" runat="server">
<ItemTemplate>
<li><%# GetEmployeeName(Container.DataItem) %></li>
</ItemTemplate>
</asp:Repeater>
</ol>
Avec une méthode GetEmployeeName dans le code behind qui nous renvoie le nom de chaque employé formaté correctement :
protected string GetEmployeeName(object dataItem)
{
Employee emp = (Employee)dataItem;
return String.Format("<strong>{0}</strong> {1}",emp.Contact.LastName.ToUpper(),emp.Contact.FirstName);
}
La liaison de données se fait ensuite dans le PreRender de la page et uniquement lors du premier chargement (Page.IsPostBack) :
protected void Page_PreRender(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
LoadEmployees();
}
}
private void LoadEmployees()
{
using (DataClassesDataContext dc = new DataClassesDataContext())
{
this.repEmployees.DataSource = dc.Employees.OrderBy(emp => emp.Contact.LastName).Take(20);
this.repEmployees.DataBind();
}
}
A ce stade le rendu navigateur est le suivant :
Figure 2 Aperçu du rendu navigateur
Affichage du détail d’un employé
Lors d’un clic sur un employé on veut afficher son adresse et rappeler son nom.
Commençons par ajouter un contrôle LinkButton à notre template afin de créer un retour serveur et être en mesure de manipuler le rendu :
<asp:Repeater ID="repEmployees" runat="server">
<ItemTemplate>
<li>
<asp:LinkButton ID="lnkEmployee" runat="server" CommandName='<%# GetID(Container.DataItem) %>' Text='<%# GetEmployeeName(Container.DataItem) %>' OnCommand="lnkEmployee_Click" ></asp:LinkButton>
</li>
</ItemTemplate>
</asp:Repeater>
A ce LinkButton on associe deux méthodes : une pour récupérer l’ID de l’élément courant et initialiser la propriété CommandName du LinkButton :
protected string GetID(object dataItem)
{
return ((Employee)dataItem).EmployeeID.ToString();
}
et une méthode abonnée à l’événement OnCommand du LinkButton :
protected void lnkEmployee_Click(object sender, CommandEventArgs e)
{
DisplayAddress(e.CommandName);
}
DisplayAddress() ayant la charge d’afficher la popup contenant l’adresse.
Mais avant tout, il nous faut d’abord ajouter de quoi afficher notre adresse dans notre page aspx.
Un simple contrôle PlaceHolder fera l’affaire. Il nous permettra d’afficher ou masquer la popup en jouant sur sa propriété Visible :
<asp:PlaceHolder ID="phAddress" runat="server" Visible="false" EnableViewState="false">
<div id="back"></div>
<div id="popup">
<p><asp:Literal ID="ltlEmployee" runat="server"></asp:Literal></p>
<span>
<asp:Literal ID="ltlAddress" runat="server"></asp:Literal>
</span>
<asp:LinkButton ID="lnkClosePopup" runat="server">Fermer</asp:LinkButton>
</div>
</asp:PlaceHolder>
Le LinkButton lnkClosePopup nous permet de fermer cette popup. La propriété EnableViewState du PlaceHolder étant positionnée à false, un retour serveur initié par un clic sur le bouton Fermer fera disparaître la popup en réinitialisant Visible à false.
Le code serveur associé pour afficher l’adresse (et le nom de l’employé sélectionné) est alors :
protected void DisplayAddress(string id)
{
using (DataClassesDataContext dc = new DataClassesDataContext())
{
Employee emp = dc.Employees.Where(e => e.EmployeeID == int.Parse(id)).First();
this.ltlEmployee.Text = string.Format("{0} {1}",
emp.Contact.FirstName,
emp.Contact.LastName.ToUpper());
Address ad = emp.EmployeeAddresses[0].Address;
this.ltlAddress.Text = string.Format("<p>{0}</p><p>{1}</p><p>{2}</p><p>{3}</p>",
ad.AddressLine1,
ad.AddressLine2,
ad.PostalCode,
ad.City);
}
this.phAddress.Visible = true;
}
Ce qui nous donne le rendu navigateur suivant lors du clic sur un employé :
Figure 3 - Aperçu du rendu navigateur
L’application est en place … reste à la rendre un peu plus sexy grâce à une petite passe de CSS :
html{background:#CECECE;}
body
{
border:solid 1px black;
margin:20px auto 20px auto;
padding:10px;
width:950px;
font-family:Tahoma;
background:#FEFEFE;
position:relative;
}
h1
{
font-size:medium;
margin:5px 0 10px 0;
text-decoration:underline;
font-weight:bold;
}
ol{list-style:none;margin:0;}
a{color:#606060;}
a:hover{color:red;}
#back
{
position:absolute;z-index:1;
top:0;left:0;
width:100%;height:100%;
background:#CECECE;
opacity:0.6;filter:alpha(opacity=60);
}
#popup
{
position:absolute;z-index:2;
top:100px;right:40%;
border:solid 1px black;
background:#606060;color:White;
padding:40px;text-align:center;
}
#popup p{margin:0;}
#popup a
{
color:White;
display:block;
position:absolute;top:3px;right:3px;
}
L’affichage de la liste est alors le suivant :
Et l’adresse est affichée sous forme de popup modale:
Le rendu visuel est bien meilleur … reste à éliminer le rafraîchissement complet de la page qui provoque malheureusement un clignotement de l’affichage assez gênant pour l’utilisateur.
C’est là qu’entre en jeux nos UpdatePanels.
On pourrait se contenter de glisser un ScriptManager et un UpdatePanel englobant l’ensemble.
Cela fonctionne mais c’est très loin d’être optimisé …
Partons plutôt du principe que moins la quantité d’information renvoyée au client est importante meilleures seront les performances et essayons une approche plus fine en se basant sur ce qui a été dit plus haut dans l’article.
Pour commencer, on n’y coupe pas, l’ajout du ScriptManager est indispensable :
<asp:ScriptManager ID="scriptManager" runat="server"></asp:ScriptManager>
On veut que le clic sur un des LinkButtons se fasse de façon asynchrone, un UpdatePanel englobant chaque élément de la liste pourrait être une solution. Cependant, il n’y a pas de modification au niveau du rendu d’un élément lorsqu’on clique dessus. Un UpdatePanel par élément de liste complexifierait donc inutilement l’arbre de contrôles côté serveur. Ensuite, il est nécessaire de mettre à jour la portion de page correspondant à la vue de détail de l’employé sélectionné. Un UpdatePanel englobant la liste des employés et un UpdatePanel englobant la vue de détail font donc l’affaire.
Cela donne ceci au niveau de notre page aspx :
<asp:UpdatePanel ID="upListeEmployees" runat="server" UpdateMode="Conditional" ChildrenAsTriggers="false">
<ContentTemplate>
<ol>
<asp:Repeater ID="repEmployees" runat="server">
<ItemTemplate>
<li>
<asp:LinkButton ID="lnkEmployee" runat="server" CommandName='<%# GetID(Container.DataItem) %>' Text='<%# GetEmployeeName(Container.DataItem) %>' OnCommand="lnkEmployee_Click" >
</asp:LinkButton>
</li>
</ItemTemplate>
</asp:Repeater>
</ol>
</ContentTemplate>
</asp:UpdatePanel>
<asp:UpdatePanel ID="upDetailEmployee" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:PlaceHolder ID="phAddress" runat="server" Visible="false" EnableViewState="false">
<div id="back"></div>
<div id="popup">
<span>
<p><asp:Literal ID="ltlEmployee" runat="server"></asp:Literal></p>
<asp:Literal ID="ltlAddress" runat="server"></asp:Literal>
</span>
<asp:LinkButton ID="lnkClosePopup" OnClick="lnkClosePopup_Click" runat="server">Fermer</asp:LinkButton>
</div>
</asp:PlaceHolder>
</ContentTemplate>
</asp:UpdatePanel>
Nos deux UpdatePanels sont en UpdateMode Conditional. ChildrenAsTriggers est à false sur la liste des employés : nous n’avons pas besoin de rafraichir la liste lorsqu’on clique sur un employé.
Le clignotement de l’interface est maintenant supprimé mais on oublie une nouvelle contrainte apportée par cette solution ! La mise à jour se fait maintenant de manière asynchrone ; alors que l’utilisateur était auparavant notifié du rechargement de la page via la barre de progression du navigateur ; il ne l’est plus. Autrement dit, si l’aller retour serveur prend du temps (réseau encombré, serveur sollicité, …) aucune information ne viendra notifier l’utilisateur que son clic à bien été pris en compte. L’utilisateur risque alors de cliquer frénétiquement sur nos LinkButtons J …
C’est là qu’intervient un autre contrôle disponible dans la liste des contrôles des extensions AJAX : l’UpdateProgress.
Sans rentrer dans les détails comme nous l’avons fait pour l’UpdatePanel, l’UpdateProgress va tout simplement afficher son contenu pendant la durée de l’appel asynchrone.
Pour ce faire, il nous suffit d’ajouter ceci dans le BODY de notre page :
<asp:UpdateProgress ID="upProgress" runat="server">
<ProgressTemplate>
<div id="backProgress"></div>
<img id="imgLoading" src="ajax-loader.gif" title="Veuillez patienter" />
</ProgressTemplate>
</asp:UpdateProgress>
Et tout comme on l’a fait pour afficher le détail sous forme de popup grâce aux styles CSS, on va afficher l’image de chargement sous forme modale, pour éviter que l’utilisateur ne déclenche un nouvel appel en cliquant à nouveau sur la liste :
#backProgress
{
position:absolute;z-index:5;
top:0;left:0;
width:100%;height:100%;
background:#CECECE;
opacity:0.6;filter:alpha(opacity=60);
}
#imgLoading
{
position:absolute;z-index:6;
top:100px;right:50%;margin-left:-16px;
display:block;
}
Ce qui nous donne le résultat ci-dessous, (j’ai simulé un traitement serveur long à l’aide d’un System.Threading.Thread.Sleep(5000)):
Notre solution est complète.
Cependant, à chaque retour serveur, qu’il soit synchrone ou asynchrone, l’ensemble de la page est exécuté.
C'est-à-dire que lorsqu’on affiche ou lorsqu’on cache la vue de détail d’un employé, la liste des employés est reconstruite en mémoire alors même que cette portion de page n’est pas rafraichie…
Dans notre cas, cette liste est rechargée à partir du ViewState du contrôle Repeater puisque le ViewState est actif sur le Repeater en question et que la méthode LoadEmployees() n’est appelée que lors du premier chargement de la page (if ( !Page.IsPostBack).
La suite de l’article va maintenant se pencher sur les pistes d’amélioration de notre solution et sur une mise en œuvre alternative un peu plus performante.
La solution en l’état est disponible dans les sources sous le nom d’exemple1.
En partant d’exemple1, cherchons des voies d’amélioration. Sachant que nous nous sommes concentré sur la réduction des données envoyées au navigateur client, cherchons à présent à diminuer la quantité de code exécutée côté serveur.
La liste étant chargée depuis le ViewState lors des appels suivant le premier chargement de la page, pourquoi ne pas tout simplement désactiver le ViewState sur le Repeater de la liste d’employés ?
<asp:Repeater ID="repEmployees" EnableViewState="false" runat="server">
Une fois le ViewState désactivé sur le Repeater et l’application lancée … mauvaise surprise, le clic sur un employé n’affiche plus la popup de détail.
Ce problème est en fait dû à la gestion événementielle en ASP.NET.
En effet, nous sommes en ASP.NET classique. Pour que la méthode associée au clic sur le LinkButton se déclenche, il est nécessaire que ce LinkButton existe dans l’arbre de contrôles côté serveur.
Lorsqu’on effectue un clic sur un des employés de la liste, on effectue en fait un POST du formulaire associé à la page. Juste avant ce POST, les variables cachées de la page __EVENTTARGET et __EVENTARGUMENT sont initialisées :
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
Elles contiennent alors l’identifiant du contrôle qui a déclenché le retour serveur ainsi qu’un deuxième argument optionnel.
Une trace de Fiddler sur l’appel asynchrone déclenché par le clic sur un employé nous donne effectivement ceci :
POST /UpdatePanel/exemple1.aspx HTTP/1.1
Accept: */*
Accept-Language: fr-FR,en-US;q=0.5
Referer: http://jt/UpdatePanel/exemple1.aspx
x-microsoftajax: Delta=true
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Cache-Control: no-cache
UA-CPU: x86
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.5.21022; MS-RTC LM 8; WWTClient2; .NET CLR 3.5.30729; .NET CLR 3.0.30618)
Host: jt
Content-Length: 466
Proxy-Connection: Keep-Alive
Pragma: no-cache
scriptManager=scriptManager%7CrepEmployees%24ctl05%24lnkEmployee&__EVENTTARGET=repEmployees%24ctl05%24lnkEmployee&__EVENTARGUMENT=&__EVENTVALIDATION=%2FwEWFQKAl%2FfJCgKb6%2F%2B3BQKY66O4CwKY67ffAwKk67v%2BDQKZ66%2BVAgKe69O6CAKe66dbApvrq%2FoKAqbrn6IPAqfrw8cHAv7AhbgFAv%2FAubgLAv%2FAvd8DAvvAsf4NAvzAtZUCAv3AqbQIAv3ArVsC%2FsCh%2BgoChcGlog8C%2BsDZxwccnPCDJ68exKSB%2BhtGIzIa9bCMhA%3D%3D&__VIEWSTATE=%2FwEPDwUKMTY2ODI3MTk4MGRkr7Dk52qsJOTkolvagZDbbr4bhnw%3D&__ASYNCPOST=true&
Si le contrôle %24ctl05%24lnkEmployee n’est pas retrouvé dans l’arbre de contrôle créés côté serveur, la méthode lnkEmployee_Click ne sera pas déclenchée.
Mais comment faire alors? … gérons l’événement de clic autrement !
Plutôt que de gérer l’événement de clic uniquement sur le LinkButton et de lui attacher une méthode lnkEmployee_Click, nous allons nous arranger pour que le clic sur un employé soit interprété comme un évènement de la page, déclenché par la page, et réceptionné directement par la page ; en prenant soin bien sûr de passer l’information indispensable qui est l’identifiant de l’employé. Ainsi, nul besoin de ré-instancier notre liste d’employés. L’évènement sera récupéré en amont.
Une classe de l’API serveur est là pour nous aider : Page.ClientScript. Le ClientScriptManager renvoyé par cette classe permet justement de générer le code client qu’il faudra ajouter sur chaque contrôle employé pour déclencher l’appel correctement.
En particulier, la méthode GetPostBackClientHyperLink génère du code JavaScript rattachable à une ancre HTML (<a>). L’exécution de ce code déclenchant un retour serveur.
Le premier argument de cette méthode correspond à la cible du retour serveur, autrement dit, le contrôle qui va recevoir l’événement.
Le deuxième argument va nous permettre de passer des paramètres éventuels. Dans notre cas, nous passerons l’identifiant de l’employé en paramètre.
Ainsi, la méthode suivante qui génère le code à attacher à une ancre HTML déclenchera un retour serveur sur la page (this), en passant en paramètre l’identifiant du client :
protected string GetLink(object dataItem)
{
Employee emp = (Employee)dataItem;
return Page.ClientScript.GetPostBackClientHyperlink(this, emp.EmployeeID.ToString());
}
Il faut maintenant que notre page soit capable de réceptionner cet événement.
Ceci va se faire grâce à l’interface IPostBackEventHandler que nous allons ajouter à notre page.
Notre page implémentant cette interface, la page doit par contrat définir la méthode RaisePostBackEvent suivante :
#region IPostBackEventHandler Members
public void RaisePostBackEvent(string eventArgument)
{
}
#endregion
Il ne nous reste plus qu’à ajouter l’ancien code contenu dans la méthode lnkEmployee_Click :
#region IPostBackEventHandler Members
public void RaisePostBackEvent(string eventArgument)
{
DisplayAddress(eventArgument);
this.upDetailEmployee.Update();
}
#endregion
Le tour est joué !
Notre application fonctionne à nouveau, tout en ayant désactivé le ViewState sur notre Repeater et en chargeant donc la liste des employés que lors du premier appel de la page.
On a donc gagné en performance.
Attention, il reste cependant un dernier point important à prendre en compte …
Que se passe-t-il si les appels asynchrones sont désactivés ?
Cela peut arriver si le navigateur client ne supporte pas le JavaScript … ou si tout simplement le rendu partiel de page a été désactivé : le ScriptManager possède une propriété EnablePartialRendering qui le permet.
Et bien, cela ne fonctionne plus correctement !
La liste des employés disparaît lors du clic sur un employé puisque la liste des employés n’est pas rechargée et que le ViewState est désactivé :
Mais la modification à apporter à notre code pour que tout rentre dans l’ordre est des plus simples !
Le ScriptManager possède une propriété IsInAsyncPostBack. Cette propriété va nous permettre de savoir si l’on est dans le cas d’un appel asynchrone ou non.
Il suffit donc de remplacer :
protected void Page_PreRender(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
LoadEmployees();
}
}
Par :
protected void Page_PreRender(object sender, EventArgs e)
{
if (!scriptManager.IsInAsyncPostBack)
{
LoadEmployees();
}
}
Notre application fonctionne donc de façon performante tout en gérant la désactivation des UpdatePanels.
Cet exemple est disponible sous le nom exemple2 dans la solution.
Nous avons passé en revue les notions de base de l’UpdatePanel et réussi à les mettre en application sur notre exemple de vue maître/détail d’employés. Ainsi, si la mise en place de l’UpdatePanel est facile, il est nécessaire de bien comprendre les propriétés UpdateMode et ChildrenAsTriggers de l’UpdatePanel afin de concevoir des solutions performantes. La compréhension du modèle événementiel d’ASP.NET nous permet de plus de concevoir des applications plus complexes mais beaucoup plus performantes en pilotant l’instanciation de nos contrôles et la mise à jour des nos UpdatePanels lors de l’exécution de notre page côté serveur.
Fiddler : http://www.fiddlertool.com/fiddler/
Documentation officielle http://www.asp.net/ajax/documentation/live/
GIF animés pour l’UpdateProgress http://www.ajaxload.info/