PostBackControl - UpdatePanel et la communication client / serveur
Les UpdatePanels permettent de définir une zone à rafraichir plutôt que toute la page lors de postback. Ils s'intègrent parfaitement à ASP.net, en effet il suffit de mettre la partie à rafraichir à l'intérieur d'un updatepanel, et si un contrôle contenu par celui-ci fait un postback alors seul le contenu de l'UpdatePanel se met à jour côté client. Aucune manipulation de JavaScript ni quoi que ce soit est nécessaire, c'est la une grande force de ASP.net Ajax.
Mais c'est aussi le problème, il n'est pas aisé pour un développeur JavaScript de demander le rafraichissement d'un UpdatePanel. Une bidouille consiste à passer par un LinkButton et de demander à JavaScript de cliquer dessus pour déclencher le postback. Comme il s'agit d'une bidouille, je me suis décidé à faire un contrôle qui ne fais que déclencher un postback : Le PostBackControl.
Quel est le but de ce contrôle ? Permettre de déclencher un postback proprement en JavaScript afin de rafraichir certains UpdatePanels.
Tout d'abord il faut créer un nouveau contrôle qui implémente l'interface IPostBackEventHandler. Cette interface est très importante puisque c'est elle qui gère le postback. Vous pouvez retrouver l'intégralité du contrôle sur ASPFr.com : PostBackControl - communication client/serveur avec les UpdatePanels et voici un exemple d'utilisation : Charger dynamiquement un contrôle à l'ouverture d'une modalpopup
Quelques précisions sur le fonctionnement interne d'un UpdatePanel
J'aimerais ajouter ici quelques précisions concernant le fonctionnement interne des UpdatePanels.
Prenoms cette page :
<asp:UpdatePanel runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:Literal runat="server" ID="litHour" />
<asp:LinkButton runat="server" ID="lbShowHour"
OnClick="lbShowHour_Click" Text="Affiche l'heure" />
</ContentTemplate>
</asp:UpdatePanel>
Que se passe-t-il lorsque l'on clique sur le bouton "Affiche l'heure" ? Bien sur le literal est mis à jour via une requête Ajax, mais comment en arrive t'on jusque là ? Que se passe-t-il en interne ?
Tout d'abord analysons le code HTML généré :
<script type="text/javascript">
Sys.WebForms.PageRequestManager.getInstance()._updateControls(
['tctl00$CPH1$ctl00'], [], [], 90);
</script>
<div id="ctl00_CPH1_ctl00">
<a id="ctl00_CPH1_lbShowHour" href="javascript:__doPostBack('ctl00$CPH1$lbShowHour','')">
Affiche l'heure</a>
</div>
On remarque l'appel à PageRequestManager._updateControls, avec en premier argument un tableau des UniqueID de tous les UpdatePanels présents dans la page.
Lorsque l'on click sur notre bouton, la méthode __doPostBack est appelé, cette méthode n'est pas la classique méthode introduite par ASP.net mais une autre version que ASP.net Ajax redéfinit. C'est ici que l'on détermine si oui ou non le postback se fera via Ajax ou alors via un classique POST. Pour définir ce comportement, la méthode va traduire le UniqueID en ClientID du contrôle déclenchant le postback en remplaçant les '$' par des '_' Ainsi il est possible de récupérer l'élément HTML. ASP.net Ajax recherche ensuite si le contrôle est contenu dans un UpdatePanel en remontant l'arbre DOM via une recherche récursive.
Concrétement dans notre cas :
- On click sur le bouton
- La méthode __doPostBack('ctl00$CPH1$lbShowHour','') (redéfinit par ASP.net Ajax) est appelé
- 'ctl00$CPH1$lbShowHour' est traduit en 'ctl00_CPH1_btnShowHour' pour récupérer l'élément HTML correspondant
- On vérifie les ID des éléments parent du contrôle pour voir s'il est contenu dans le tableau du premier argument de la méthode PageRequestManager._updateControls. Si oui alors le contrôle est contenu dans un UpdatePanel, il s'agit alors d'un AsyncPostBack.
Le cas plus haut est assez simple mais comment cela fonctionne lorsque j'utilise des AsyncPostBackTrigger et PostBackTrigger :
<asp:UpdatePanel runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:Literal runat="server" ID="litHour" />
<asp:LinkButton runat="server" ID="lbShowHourPostBack"
OnClick="lbShowHour_Click" Text="Affiche l'heure" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="lbShowHourAsync" />
<asp:PostBackTrigger ControlID="lbShowHourPostBack" />
</Triggers>
</asp:UpdatePanel>
<asp:LinkButton ID="lbShowHourAsync" runat="server"
OnClick="lbShowHour_Click" Text="Affiche l'heure" />
C'est ici qu'interviennent les 2 autres arguments de la méthode _updateControls :
<script type="text/javascript">
Sys.WebForms.PageRequestManager.getInstance()._updateControls(
['tctl00$CPH1$ctl00'], ['ctl00$CPH1$lbShowHourAsync'],
['ctl00$CPH1$lbShowHourPostBack'], 90);
</script>
Comme vous vous en doutez, les nouveaux arguments seront utilisé lors de la recherche de l'UpdatePanel parent (recherche récursive).
Quel rapport avec mon contrôle ? Puisque celui-ci n'a pas besoin de rendu HTML je l'ai simplement fait hériter de Control, il n'est donc lié à aucun élément HTML, la recherche récursive de l'UpdatePanel ne peut alors pas s'effectuer, ASP.net fait un classique postback. J'ai donc enregistré manuellement le contrôle comme faisant un AsyncPostBack.
ScriptManager sc = ScriptManager.GetCurrent(this.Page);
if (sc != null)
sc.RegisterAsyncPostBackControl(this);
Plus concrétement, cela a pour effet d'ajouter l'id de mon contrôle dans tableau du 2ème argument de la méthode _updateControls. Ainsi, j'arrive bien à faire une requête Ajax. Par contre l'UpdatePanel contenant le contrôle n'est pas rafraichis. Pourquoi ? Pour le savoir il faut s'interesser au contenu POST de la requête XHR d'un AsyncPostBack.
Parmi les champs POST envoyé il y a un champ caché qui porte le nom du ScriptManager, voici ces valeurs.
#cas du Linkbutton
ctl00$SC1 ctl00$CPH1$ctl00|ctl00$CPH1$lbShowHour
#cas du PostBackControl
ctl00$SC1 ctl00$SC1|ctl00$CPH1$pbcTest
On voit qu'il y a 2 valeurs séparé par un pipe (|). La première valeur est l'UpdatePanel qui réclame l'AsyncPostback alors que le second correspond au contrôle le déclenchant. Le problème se situe ici : c'est JavaScript qui décide quel UpdatePanel il doit rafraichir. Notre contrôle ne générant aucun élément HTML, JavaScript n'a aucun moyen de savoir à quel UpdatePanel il appartient.
Les solutions possibles sont de rajouter manuellement un trigger dans l'UpdatePanel qui contient le PostBackControl ou alors rajouter automatiquement un trigger en regardant les parents jusqu'a obtenir un UpdatePanel au niveau du controle C# voir même d'appeler la méthode Update de l'UpdatePanel à rafraichir dans l'événement CallBack. Mais la solution que j'ai choisi est de générer du HTML, pour cela il nous suffit d'hériter de WebControl plutôt que de Control, ainsi un élément span avec un ID sera généré et il n'y aura plus de problème.
Mais est-ce normal que ASP.net Atlas ne fasse pas automatiquement cette recherche ? Il me semble que dans les versions beta d'Atlas, cette recherche était faite mais ca a été supprimé pour des raisons de performances, en effet l'utilisation d'un contrôle qui ne génère aucun HTML mais effectue un PostBack est très rare ...
[Edit]
Voici un exemple d'utilisation : Charger dynamiquement un contrôle à l'ouverture d'une modalpopup