MSDN Magazine > Home > Issues > 2008 > Octobre >  ASP.NET AJAX 4.0 : Nouvelle prise en charge AJA...
ASP.NET AJAX 4.0
Nouvelle prise en charge AJAX pour les applications Web pilotées par les données
Bertrand Le Roy

Téléchargement de code disponible ici : MSDN Code Gallery (188 Ko)
Parcourez le code en ligne

Cet article est basé sur des versions préliminaires d'ASP.NET. Toutes les informations contenues dans le présent document peuvent faire l’objet de modifications.

Cet article aborde les sujets suivants :
  • Manipulation de données côté serveur
  • UpdatePanel et le client
  • Réduction des publications et des charges
  • Rendu de modèles côté client
Cet article utilise les technologies suivantes :
ASP.NET AJAX 4.0
AJAX est une plate-forme Web passionnante pour plusieurs raisons. Avec AJAX, de nombreuses tâches traditionnellement exécutées sur le serveur sont effectuées dans le navigateur, ce qui se traduit par un nombre d'allers-retours au serveur et une consommation de bande passante réduits, et des interfaces utilisateur Web plus rapides et réactives. Bien que ces avantages résultent du transfert d'une grande quantité de travail vers le client, le navigateur ne constitue toujours pas l'environnement de choix pour un grand nombre de développeurs, qui préfèrent avoir à leur disposition toute la puissance et la flexibilité des applications de serveur.
La solution utilisée jusqu'à présent fait appel au contrôle UpdatePanel, qui permet aux développeurs de créer des applications AJAX tout en conservant la gamme complète des outils de serveur. Toutefois, UpdatePanel est toujours fortement basé sur le modèle de publication traditionnel : en effet, une requête UpdatePanel est toujours une publication complète. En fait, avec UpdatePanel, le formulaire entier (y compris ViewState) est publié sur le serveur, presque la totalité du cycle de vie de la page y est exécutée et le rendu a toujours lieu sur le serveur. Visiblement, cette méthode démonte l'une des principales raisons de passer à AJAX. Les seuls vrais avantages ici résident dans le fait que XmlHttpRequest est utilisé au lieu d'une requête HTTP POST normale et que seules les parties de la page qui ont été mises à jour et le ViewState sont renvoyés au client. Ainsi, la réponse est beaucoup plus petite, mais la requête ne l'est pas.
Une approche AJAX pure sera presque toujours plus performante que l'approche UpdatePanel. Dans une solution purement AJAX, le rendu a lieu sur le client et le serveur renvoie seulement les données, qui sont généralement beaucoup plus réduites que leur équivalent HTML. Cette approche peut également réduire considérablement le nombre de requêtes réseau : le fait d'avoir les données sur le client permet à une bonne partie de la logique de l'interface utilisateur de l'application de s'exécuter dans le navigateur.
Toutefois, le principal problème de l'approche AJAX pure est que le navigateur ne dispose pas de tous les outils nécessaires pour transformer les données en HTML. À la base, elle ne dispose que de deux méthodes brutes pour effectuer une telle opération : InnerHTML, qui remplace la totalité du contenu d'un élément par la chaîne HTML que vous fournissez, et les API DOM (Document Object Model) légèrement plus lentes, qui fonctionnent sur les balises et les attributs (identiques en termes de niveau d'abstraction à HtmlTextWriter).
Dans cet article, j'examinerai trois itérations d'une page écrite avec une publication classique puis avec UpdatePanel et enfin avec une approche AJAX pure pour vous montrer comment les techniques utilisées sur le serveur peuvent parfois être plus performantes sur le client. Les deux premiers exemples peuvent être créés actuellement avec ASP.NET 3.5 SP1, disponible publiquement, tandis que la troisième version utilisera certaines des nouvelles fonctionnalités de client d'ASP.NET 4.0.

Page de détails maître basée sur les publications
La page que je créerai comprendra une liste de produits qui, lorsqu'elle est sélectionnée, affichera une description détaillée de chaque produit dans un panneau à droite. J'utiliserai l'exemple de base de données AdventureWorks que vous pouvez télécharger sur le site go.microsoft.com/fwlink/?LinkId=124953. Je ne créerai qu'une couche de données rudimentaire à l'aide de LINQ to SQL vu que les couches de données ne constituent pas le principal sujet de cet article.
Je commencerai par ajouter le fichier .mdf d'AdventureWorks au dossier App_Data de l'application. Ensuite, j'ajouterai tout simplement un nouveau fichier .dbml « de classes LINQ to SQL » et déposerai les tables Product, ProductPhoto et ProductProductPhoto ainsi que la vue vProductModelCatalogDescription de l'Explorateur de serveurs sur la surface de conception. La couche de données qui en résulte est illustrée à la figure 1.
Figure 1 Architecture de la couche de données (Cliquez sur l'image pour l'agrandir)
La page consistera en deux volets d'affichage, l'un pour la liste des produits et l'autre pour les détails des produits. La figure 2 présente la page rendue. À la première requête de la page, la liste des produits est liée aux données à l'aide du code suivant :
private void BindProductList() {
    ProductList.DataSource = from p in AdventureWorksContext.Products
                             where p.ProductSubcategoryID == 1 
                             //Mountain bikes
                             orderby p.Name
                             select p;
    ProductList.DataBind();
}
Figure 2 Liste de vélos et détails (Cliquez sur l'image pour l'agrandir)
La liste même et son modèle sont illustrés à la figure 3. Le code qui interroge la base de données se trouve dans le projet téléchargeable et est relativement simple ; il envoie des requêtes à la base de données concernant les produits de la catégorie Vélo tout-terrain et lie le contrôle ListView à cette catégorie. Bien sûr, j'aurais pu utiliser un contrôle de source de données pour effectuer la même tâche mais je trouve l'approche à base de code plus flexible et prévisible. Vos résultats peuvent varier si vous êtes plus porté sur le mode Conception.
Le travail de création du balisage HTML à partir du jeu de données est effectué par le contrôle ListView, ce qui est très pratique et n'exige aucun effort. Il me suffit de fournir le modèle de ce HTML dans les propriétés LayoutTemplate et ItemTemplate (voir la figure 3). Ce modèle affichera la liste de produits sous forme de liens dans une liste désordonnée (balises UL et LI).
<asp:ListView ID="ProductList" runat="server"
    DataKeyNames="ProductId"
    OnSelectedIndexChanging="ProductList_SelectedIndexChanging"
    OnSelectedIndexChanged="ProductList_SelectedIndexChanged">
    <LayoutTemplate>
        <ul ID="itemPlaceholderContainer" runat="server">
            <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
        </ul>
    </LayoutTemplate>
    <ItemTemplate>
        <li><asp:LinkButton runat="server" ID="Select" CommandName="Select" 
                                         Text='<%# Eval("Name") %>' /></li>
    </ItemTemplate>
</asp:ListView>
Notez que les liens mêmes ne sont pas de simples liens mais plutôt des contrôles LinkButton, ce qui signifie qu'ils publieront sur la page au lieu de naviguer vers une autre page. Il s'agit en fait de liens qui fonctionnent comme des boutons. Bien sûr, il est tout à fait possible d'améliorer l'accessibilité de la page en l'absence de JavaScript en remplaçant ces contrôles LinkButton par des contrôles Button normaux.
La fonctionnalité clé des LinkButtons que j'utilise ici réside dans le fait qu'au lieu d'attacher un gestionnaire d'événements à chacun des boutons, je définis la propriété CommandName sur « Select ». Ainsi, lorsque le bouton fait l'objet d'un clic, il propage la commande dans l'arborescence de contrôle jusqu'à ce qu'elle trouve un contrôle capable de la traiter. Il s'agit là d'une fonctionnalité très puissante qui permet à un élément d'interface utilisateur arbitraire d'envoyer des commandes à ses contrôles parent en se contentant de connaître uniquement les commandes et arguments auxquels ils s'attendent. C'est ce qui permet à des contrôles de données puissants tels que ListView d'assurer au développeur un contrôle total du balisage. Vous verrez comment cela se traduit par des concepts de navigateur similaires lorsque je créerai la version AJAX pure de cette même page.
À ce stade, j'ai une liste de produits qui prend en charge la sélection sans avoir écrit de code. Je pourrais continuer sur cette voie sans code et utiliser les contrôles DataSource et ControlParameters pour lier la clé de données sélectionnée de la liste à la clé de données sélectionnée de la vue de détails, mais j'ai choisi de le faire dans le code. Ensuite, je traite l'événement SelectedIndexChanged de la liste et appelle la méthode BindProductDetails avec l'ID de produit approprié :
protected void ProductList_SelectedIndexChanged(object sender, 
                                                 EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    BindProductDetails(productId);
}
BindProductDetails interroge la base de données pour obtenir les informations et photos du produit et les lie aux contrôles correspondants dans la vue de détails.
Les photos sont servies par un gestionnaire simple qui envoie une requête à la base de données pour obtenir les octets d'image et les copie sur le flux binaire de la réponse (voir la figure 4). Ce gestionnaire sera utilisé par chacune des trois versions de la page. J'ai maintenant une vue de détails maître pilotée par les données pour mes produits, écrite entièrement avec un mélange de code serveur impératif et déclaratif, mais plusieurs choses pourraient être améliorées.
public void ProcessRequest (HttpContext context) {
    int id;
    if (int.TryParse(context.Request.QueryString["id"], out id)) {
        context.Response.ContentType = "image/gif";
        AdventureWorksDataContext dc = new AdventureWorksDataContext();
        var bytes = dc.ProductPhotos
            .Where(p => p.ProductPhotoID == id)
            .Single().LargePhoto;
        context.Response.OutputStream.Write(bytes.ToArray(), 0, bytes.Length);
    }
    else {
        throw new HttpException(404, "Image not found");
    }                
}
Cette page Web Forms est extrêmement typique en ce sens qu'il s'agit d'une page avec état en termes de volume de l'état. Une vérification rapide de la source affichée dans le navigateur indique 4 Ko de ViewState en raison du fait que les différentes vues se souviennent toutes de leur état interne et l'intègrent à chaque publication.
En même temps, la page ne donne pas d'indice concernant son état actuel ; quoi que vous fassiez sur la page, l'URL de la barre de navigation du navigateur demeure « 1_WebForm.aspx ». Si un utilisateur ajoute cette page aux Favoris, il verra toujours, au départ, la page sans aucun détail sur le produit.
Dans ce simple exemple, il est possible de corriger cela en modifiant les liens dans la liste de produits de sorte qu'il s'agisse de liens réguliers et non de contrôles LinkButton, et en remplaçant les publications de sélection par une navigation simple vers une page de détails (je pourrais même désactiver ViewState et économiser environ 4 Ko deux fois par aller-retour). Si vous avez suivi le récent développement de la bibliothèque MVC (Model View Controller) d'ASP.NET (voir msdn.microsoft.com/magazine/cc337884), vous avez probablement deviné qu'il s'agit d'un cas typique où l'approche MVC est tout à fait judicieuse.
Les approches basées sur la navigation améliorent aussi considérablement la recherche sur le site (sujet qui pourrait faire l'objet d'un article entièrement différent). Ceci dit, les applications pilotées par les données typiques seront beaucoup plus complexes que cet exemple, et la simple navigation ne constitue souvent pas la bonne méthode de création du flux de l'interface utilisateur. C'est pour cela que, même avec cette simple application, je consacrerai du temps à vous montrer comment les concepts de publication et de formulaires Web sont rendus et améliorés avec AJAX.
La publication et la navigation par liens risquent également de perturber l'expérience utilisateur plus qu'il n'est souhaitable : durant la publication et la navigation, l'interface utilisateur est gelée et aucune autre interaction utilisateur n'est possible avant que le serveur ne renvoie un nouveau contenu qui doit alors être affiché pour remplacer la totalité du document, perdant parfois des éléments subtils de l'état tels que la position du défilement.
Un autre problème est dû au fait que les utilisateurs savent exactement ce qu'ils veulent que le bouton Précédent et l'historique fassent. Malheureusement, dans le modèle de publication, vous avez peu ou pas de contrôle sur ce qui est placé dans l'historique ou sur ce qui se passe lorsque l'utilisateur appuie sur Précédent, Suivant ou Actualiser. Dans l'idéal, les changements d'état et la création de points d'historique devraient être laissés à l'appréciation des développeurs, mais dans une application de publication, presque n'importe quelle interaction utilisateur créera une entrée dans l'historique du navigateur.

Version UpdatePanel
Un simple moyen d'améliorer cette page consiste à utiliser un UpdatePanel. Un UpdatePanel vous permet de délimiter les parties de la page qui changent lorsque les actions de l'utilisateur déclencheraient normalement une publication. Dans ce cas très simple, la partie de la page que je voudrais mettre à jour est la vue de détails. Pour permettre les mises à jour partielles d'UpdatePanel, il me suffit d'ajouter un contrôle ScriptManager à la page, juste après la balise de formulaire, comme ceci :
<asp:ScriptManager ID="ScriptManager1" runat="server"/>
Je dois également ajouter l'UpdatePanel à proprement parler, autour de la vue de détails :
<asp:UpdatePanel ID="UpdatePanel1" runat="server" RenderMode="Inline">
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="ProductList" 
            EventName="SelectedIndexChanged" />
    </Triggers>
    <ContentTemplate>
        <div class="float" id="productDetails">
            <fieldset>
            ...
            </fieldset>
        </div>
    </ContentTemplate>
</asp:UpdatePanel>
Notez que cet UpdatePanel a un déclencheur qui surveille l'événement SelectedIndexChanged de la liste de produits. Ceci n'est pas nécessaire lorsque tous les contrôles qui pourraient déclencher une mise à jour partielle se trouvent eux-mêmes dans l'UpdatePanel, mais en l'occurrence, la liste de produits doit rester à l'extérieur de l'UpdatePanel parce que son rendu n'a pas besoin d'être mis à jour lorsque son événement SelectedIndexChanged a lieu. Si je ne fournissais pas de déclencheur, une publication ordinaire aurait lieu à la place de la mise à jour partielle. En outre, lorsque vous utilisez UpdatePanel, n'oubliez pas de toujours identifier tous les contrôles de publication susceptibles de déclencher une mise à jour partielle. Si vous ne le faites pas, la page risque d'utiliser des publications ordinaires sans aucune raison apparente.
Voilà tout ce qu'il faut faire pour transformer une publication Web Form classique qui a l'apparence d'AJAX. Toutefois, il me semble que je ne dispose pas vraiment de toutes les fonctionnalités dont j'ai besoin. L'un des problèmes est que j'ai perdu le peu de prise en charge que j'avais pour le bouton Précédent. Si l'utilisateur appuie sur ce bouton après avoir examiné quelques vélos, il retournera au site qu'il a visité avant le vôtre. S'il appuie alors sur le bouton Suivant, il reviendra à l'état par défaut de l'application (dans ce cas, une liste de produits sur laquelle aucun produit n'est sélectionné).
Fort heureusement, ASP.NET 3.5 SP1 offre un moyen simple de réintégrer la prise en charge du bouton Précédent à la page. ScriptManager est désormais doté d'une propriété EnableHistory, d'une méthode AddHistoryPoint et d'un événement Navigate très commodes qui, ensemble, permettent au développeur d'applications de contrôler l'historique du navigateur bien au-delà de ce que pouvaient permettre les publications ordinaires. Cette fonctionnalité non seulement rétablit ce qui a été perdu en utilisant UpdatePanel, mais le fait sous une forme beaucoup plus puissante.
La grande différence par rapport aux publications ordinaires réside dans le fait que je peux maintenant décider exactement ce qui constitue une modification de l'état de l'application et éliminer toute interaction utilisateur que je considère comme étant moins importante. Je bénéficie également de capacités de création de signets accrues, ce qui me permet de m'assurer que les entrées figurant dans le menu déroulant de l'historique du navigateur sont lisibles et pertinentes.
Pour ajouter la gestion de l'historique à la page, je dois déterminer quelles informations doivent être conservées lorsque j'utilise un signet. Dans ce cas-ci, une seule information pertinente doit être entrée dans l'état : l'ID du produit actuellement sélectionné.
Je dois intercepter tout événement appelé à modifier cet état. En fait, le seul événement que je dois traiter est le même que celui que j'ai utilisé comme déclencheur précédemment : l'événement SelectedItemChanged de la liste de produits. Je traite déjà cet événement pour lier à nouveau la vue de détails, et il me suffit donc d'ajouter du code pour créer un nouveau point d'historique à chaque fois que l'événement est déclenché :
protected void ProductList_SelectedIndexChanged(object sender, 
                                                       EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    var product = BindProductDetails(productId);
    if (ScriptManager1.IsInAsyncPostBack 
                                   && !ScriptManager1.IsNavigating) {
         ScriptManager1.AddHistoryPoint("product", 
           productId.ToString(), "AdventureWorks - " + product.Name);
    }
}
Pour garantir que l'événement n'a pas été déclenché en raison d'un retour de l'utilisateur à un état précédent de l'application, le code vérifie que la requête fait partie d'une publication asynchrone et non d'une opération de navigation. Si je n'effectuais pas cette vérification, je créerais un nouveau point d'historique qui remplacerait tout historique de navigation précédent pouvant exister sur le navigateur.
Une fois cette vérification effectuée, je peux appeler la méthode AddHistoryPoint en toute sécurité, en transmettant le seul élément d'état qui m'intéresse, c'est-à-dire l'ID de produit, avec le nom de paramètre « product ». Ce nom est celui qui sera utilisé dans l'URL modifiée, comme vous le verrez. La valeur elle-même doit être transformée en chaîne. Pensez à l'état de l'historique comme à une autre forme de chaîne de requête. La dernière information que je donne à la méthode est le titre du document. C'est une occasion idéale pour améliorer l'expérience utilisateur car l'utilisateur pourra voir des informations pertinentes dans le menu déroulant de navigation de l'historique, ce qui l'aidera à parcourir l'application (voir figure 5).
Figure 5 Menu déroulant Historique (Cliquez sur l'image pour l'agrandir)
Cet état est maintenu par le biais du hachage de l'URL du navigateur (la partie qui vient après le signe # qui, au départ, a été conçue pour faciliter la navigation dans le document). L'intérêt d'utiliser un tel support de stockage réside dans le fait qu'il s'agit de la seule méthode permettant d'ajouter une entrée d'historique sans avoir à naviguer loin de la page et de ses états JavaScript et DOM (le navigateur ne permettant l'addition d'un autre élément d'historique que si l'URL a été modifiée). Cette méthode s'accompagne d'une contrainte, à savoir que vous enregistrez l'état dans l'URL, et que cette URL dispose d'un espace limité. Certains navigateurs risquent de rejeter les URL dont la taille est supérieure à 1 Ko. S'il vous faut plus d'un Ko, cela peut signifier que vous n'avez pas sélectionné des éléments d'informations pertinents en tant qu'état et vous devrez alors effectuer un peu de refactorisation.
Notez que j'utilise l'ID du produit, qui est une donnée relativement petite, et pas son nom complet qui, bien que plus convivial, est généralement plus gros. Un espace insuffisant dans l'URL peut également indiquer que des Web Forms et ViewState ordinaires peuvent être plus appropriés à votre conception qu'AJAX et l'historique.
La deuxième partie de l'énigme est liée à ce que l'état que je viens d'enregistrer doit être restauré lorsque l'historique est utilisé. Pour ce faire, je traite l'événement Navigate sur ScriptManager (voir figure 6). Dans le code, je traite d'abord le cas où il n'y a pas d'état. Ceci permet de retourner à l'état par défaut de la page lorsqu'on remonte jusqu'à la requête GET. Ceci peut sembler un peu insolite si vous ne savez pas que l'état est en fait restauré par une nouvelle publication. Dans ce cas, l'état « précédent » de la publication, celui qui est automatiquement restauré par l'infrastructure, est l'état « suivant » dans la chronologie de l'historique du navigateur. Je dois donc supprimer cet état restauré et le remplacer par l'état par défaut.
protected void ScriptManager_Navigate(object sender, HistoryEventArgs e) {
    var productIdString = e.State["product"];
    if (productIdString == null) {
        ProductList.SelectedIndex = -1;
        ProductDetails.DataSource = null;
        ProductDetails.DataBind();
        ProductModelDetails.DataSource = null;
        ProductModelDetails.DataBind();
        ProductPhotoList.DataSource = null;
        ProductPhotoList.DataBind();
        Page.Title = "AdventureWorks";
    }
    else {
        var productId = int.Parse(productIdString);
        var product = BindProductDetails(productId);
        ProductList.SelectedIndex = (
            from p in AdventureWorksContext.Products
            where p.ProductSubcategoryID == 1 // Mountain bikes
            orderby p.Name
            select p).ToList().IndexOf(product);
        BindProductList();
        Page.Title = "AdventureWorks - " + product.Name;
    }
}
Ensuite, l'état lui-même doit être considéré comme étant fourni par l'utilisateur et doit ainsi être validé, ce que je fais en l'analysant comme un entier. Enfin, je restaure le titre de la page lorsque je restaure l'état, et je réinitialise en plus les états de la liste et des détails.
Si vous utilisez la page après ces modifications, l'URL du navigateur changera à chaque fois qu'un produit sera sélectionné et aura l'aspect suivant :
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&5YLQHC81D2
OEdJU/9ZBdHUip1qx3ooPKDhCLgKogupQ=
C'est moche et pas très lisible, non ? Cela est du au fait que l'infrastructure considère les données fournies par l'utilisateur comme étant dangereuses par défaut, et elle hache donc l'état pour éviter les altérations. Toutefois, dans de nombreux cas, le développeur préférera des URL relativement lisibles et moins effrayantes, même s'il doit alors valider l'état à partir du code et permettre à l'utilisateur de le modifier. Les URL modifiables sont même considérées comme un avantage dans certaines situations (la bibliothèque MSDN Library en est un bon exemple ; elle permet à l'utilisateur de créer sa propre URL, telle que msdn.microsoft.com/library/system.web.ui.scriptmanager.aspx, à partir d'un schéma prévisible parce qu'elle rend la navigation beaucoup plus facile).
Pour permettre ce type de scénario, ScriptManager expose la propriété booléenne EnableSecureHistoryState. En définissant simplement cette propriété sur false, je rends mes URL beaucoup plus conviviales, comme celle qui suit :
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&product=776
Il en résulte une page beaucoup plus fluide qui ne se contente pas de ressembler à une version AJAX du formulaire Web, mais une page qui comporte de nombreuses autres fonctionnalités pratiques telles que la capacité de création de signets et le traitement optimal du bouton Précédent. Et tout ceci a été accompli sans écrire une ligne seule de JavaScript.

Version AJAX pure
Bien que la version UpdatePanel de la page soit tout à fait acceptable, je traîne encore le poids de ViewState. Pour le réduire un peu, je dois transférer davantage de logique vers le client. Pour ce faire, je dois commencer par écrire un peu de JavaScript amusant.
Il est tout à fait possible d'écrire une version AJAX pure avec ASP.NET AJAX 3.5 SP1, mais il serait fastidieux de formater les données en HTML. Il existe deux façons de transformer des données en HTML.
La première, utilisée par la plupart des moteurs de modèles clients, consiste à concaténer des chaînes en alternant le contenu des modèles statiques avec le contenu des données dynamiques. Cette méthode semble relativement simple et rapide puisqu'elle utilise innerHTML comme seule méthode d'interaction avec le DOM. Elle comporte, toutefois, quelques problèmes.
Par exemple, elle ne protège pas correctement contre les attaques par injection de code : si vous envisagez de générer du HTML en concaténant des chaînes, vous devrez encoder toutes les données avant de les utiliser. En revanche, si vous introduisez un guillemet dans un attribut ou une balise de script dans un nœud de texte, par inadvertance ou de façon délibérée, vous risquez de déclencher une exécution de code arbitraire. L'encodage est plus compliqué qu'il n'y paraît parce que vous aurez peut-être besoin de différents algorithmes d' encodage selon que vous injectez un attribut de texte, un attribut d'URL ou un nœud de texte.
Un moteur de modèles a également besoin d'un langage d'expression ; il est certes facile d'injecter des champs de données simples sans modifications, mais cela ne suffit pas dans tous les cas. Souvent, vous aurez besoin d'appliquer une chaîne de format, de combiner plusieurs champs et plus généralement, de traiter les données avant de les afficher. Pour ce faire, vous pourriez transformer les données avant de les intégrer au modèle, mais il est plus facile et plus pratique d'intégrer cette capacité au moteur de modèles. Une fois que vous commencerez à ajouter des fonctionnalités telles que le formatage, vous vous apercevrez rapidement que vous avez besoin de la flexibilité d'un langage d'expression complet.
La possibilité d'insérer du code et du balisage (par exemple avec les blocs <% %> dans ASP) permet d'obtenir des scénarios intéressants tels que la répétition du balisage à l'aide d'une boucle autour d'un fragment HTML ou le rendu conditionnel à l'aide d'une simple instruction. Ce scénario, lui aussi, nécessite un langage complet pour être vraiment utile.
Enfin, le HTML ne représente qu'une partie de l'histoire. Les applications AJAX concernent surtout le contenu actif, et pas uniquement les mises à jour côté client du DOM. Une fois votre HTML généré, vous devez encore associer les événements aux éléments et joindre les contrôles et les comportements. Vous pouvez le faire avec un peu de code après la génération du HTML, mais cela crée une asymétrie désagréable entre le HTML, qui devient très facile, et la logique, qui devient plus complexe et nécessite une connaissance de la structure du modèle.
En d'autres termes, pour joindre le comportement, vous devez savoir où le placer, ce qui à son tour signifie que toute modification de la structure du balisage du modèle HTML nécessitera des modifications du code qu'il active. Il existe des moyens de rendre ce couplage plus souple, mais une solution encore meilleure consisterait à intégrer l'activation de contenu au moteur de modèles.
L'autre façon de générer du HTML à partir des données est de manipuler les API DOM et de créer directement les éléments, attributs et nœuds de texte à partir du code. À première vue, ce choix ne semble pas très attrayant pour plusieurs raisons. Certes, il présente l'avantage d'être apprécié des fanatiques des normes, mais pour une raison étrange, il est beaucoup plus lent qu'innerHTML. Toutefois, la raison principale pour laquelle il n'est pas souvent utilisé est que les API DOM ne sont pas très expressives, et que le code qui en résulte est difficile à lire et encore plus difficile à maintenir, du moins sans aide et sans abstractions supplémentaires. Certains kits de ressources tels que jQuery fournissent d'excellentes abstractions et rendent l'opération beaucoup plus amusante, mais même avec de tels outils, elle demeure plus compliquée qu'elle ne devrait l'être (c'est pourquoi même jQuery possède plusieurs plug-ins de modèles).
Certains d'entre vous savent peut-être que Microsoft proposait déjà un moteur de modèles dans ASP.NET AJAX Futures, mais celui-ci était trop lent et de conception trop complexe, et nous voulions faire beaucoup mieux. Cette première tentative ratée nous a été très utile car elle nous a beaucoup appris sur ce que nous ne voulions pas que soit la nouvelle version (c'est-à-dire lente et complexe).
L'équipe de développement a testé de nombreuses conceptions pour un nouveau moteur de modèles pour ASP.NET AJAX, de la concaténation de chaînes aux manipulations DOM complètes, et nous avons évalué leurs performances, leur simplicité et leur flexibilité. Nous les avons également comparées en termes de scénarios dont elles empêchaient le déroulement. Il n'existe pas de solution idéale, mais nous avons choisi celle qui semblait représenter le meilleur compromis.
Le principe du nouveau moteur est simple : nous prenons votre code de modèle, qui contient le HTML, les champs de données, les expressions, l'instanciation de composant déclarative et le code impératif, et nous transformons tout cela, comme par magie, en code JavaScript qui crée le HTML équivalent. Cela a l'air relativement simple et ça l'est (enfin, si l'on oublie les petites bizarreries qui apparaissent par la suite au niveau du navigateur). Nous perdons un peu en performances lorsque nous utilisons les API DOM, mais si nous sommes prudents et créons les éléments à l'extérieur du DOM, et que nous ajoutons un minimum d'éléments aussi tard que possible, les pertes de performances peuvent être négligeables et la remarquable flexibilité qu'il génère vaut bien les compromis. Tous les problèmes liés à la méthode de concaténation de chaînes semblent s'évaporer naturellement.
Les attaques par injection de code n'ont qu'à bien se tenir ! Vu que j'utilise du code pour créer des nœuds de texte et définir des valeurs d'attributs, je n'ai pas besoin d'encoder quoi que ce soit car les API que j'utilise sont déjà sécurisées. Ceci est comparable à l'utilisation de paramètres SQL par rapport à la création de SQL avec la concaténation de chaînes. Aucun développeur sain d'esprit n'adopte plus cette dernière solution, alors pourquoi prendre un risque analogue ici ?
Qui donc a besoin d'un nouveau langage d'expression ? Nous en avons déjà un : JavaScript. Lorsque vous transformez le balisage de modèle en code JavaScript, quoi de plus simple que d'injecter des expressions JavaScript dans le code que vous générez ?
Pour faciliter les tâches de développement de modèles les plus courantes, à savoir l'injection unique et unidirectionnelle de champs de données (ce qui, sur le serveur, est exprimé par "<%= expression %>" et dans notre système par "{{ expression }}"), j'utilise une fonctionnalité JavaScript qui est souvent méprisée : le mot-clé « with ». Cela m'évite de recourir à des expressions telles que "{{ dataItem.myField }}" pour injecter un champ de l'élément de données associé à l'instance de modèle. Grâce au mot-clé « with », vous pouvez entourer le code généré pour le modèle de quelque chose comme "with(dataItem) {…}" de sorte que tout membre de l'élément de données soit promu à la partie supérieure de la fonction de modèle, ce qui simplifiera l'injection d'expressions et la réduira au format "{{ myField }}".
Vous pouvez injecter du comportement dans le modèle de deux façons. Tout d'abord, vous pouvez écrire un code $attachEvent et $create à partir de l'événement itemCreated ou en ligne dans le modèle à l'aide d'une variable spéciale $element qui est disponible dans le modèle et qui référence le dernier élément créé. Sinon, vous pouvez également utiliser la syntaxe déclarative que nous fournissons. Par exemple, si vous voulez ajouter un comportement de remplissage automatique et de filigrane à une balise d'entrée, vous écrirez quelque chose comme ceci :
<body xmlns:sys="javascript:Sys"
 xmlns:autocomplete="javascript:AjaxControlToolkit.AutoCompleteBehavior"
 xmlns:watermark="javascript:AjaxControlToolkit.extBoxWatermarkBehavior">
...
<input id="search" sys:attach="autocomplete,watermark"
 autocomplete:servicepath="SearchAutoComplete.asmx"
 watermark:watermarktext="Type your search terms here" />
Ici, j'inscris le préfixe pour chaque comportement déclaratif sur le HTML ou la balise du corps (ou sur la balise parente pour le modèle) en utilisant la déclaration d'espace de noms XHTML xmlns. Cela me permettra d'étendre le balisage XHTML de façon standard, et c'est similaire aux directives @Register pour le code de serveur. La partie qui vient après "xmlns:" est le préfixe qui sera associé à chaque comportement ou contrôle. L'URL de l'espace de noms utilise le protocole "javascript:" pour mapper le préfixe sur un type de JavaScript spécifique. L'espace de noms "sys" est un espace de noms système spécial qui doit être mappé sur l'espace de noms Sys, qui est l'espace de noms racine dans AJAX.
L'instanciation elle-même se fait par le biais de l'attribut spécial sys:attach, dont la valeur est une liste délimitée par des virgules des préfixes des comportements ou des contrôles à instancier et associer à l'élément. Je peux ensuite définir des propriétés pour tous ces comportements sans risquer de créer un conflit avec les attributs HTML ordinaires ou les autres comportements sur le même élément parce qu'ils sont bien différenciés par espace de noms.
L'une des fonctionnalités les plus élégantes du moteur est que la compilation du modèle dans le code JavaScript est vraiment comparable à une étape de compilation réelle. Cela signifie qu'elle ne doit se produire qu'une seule fois par modèle, ce qui permet d'exécuter un certain nombre de tâches à l'avance au lieu de les exécuter à chaque fois que le modèle est instancié. Mais assez de théorie pour le moment. Comment cela s'applique-t-il à notre page de détails/maître ?

Modèles de version AJAX
Le modèle pour la liste de produits est très simple :
<ul id="productListTemplate" class="sys-template">
    <li>
        <a href="{{ String.format('3_Client.aspx?product={0}',
        ProductID) }}">{{ Name }}</a>
    </li>
</ul>
Le modèle d'élément est un élément de liste contenant un lien simple. Le texte du lien est simplement le nom du produit ("{{ Name }}") et l'attribut href est une chaîne formatée créée à partir de l'ID de produit avec du simple JavaScript :
"{{ String.format('5_Client.aspx?product={0}', ProductID) }}"
La classe "sys-template" est définie dans le CSS de la page pour masquer le modèle du rendu initial de la page. Le code compilé de ce modèle simple est illustré à la figure 7. La vue de détails est un peu plus complexe et contient en fait du code en ligne (voir la figure 8). J'aurais pu utiliser un modèle imbriqué pour afficher la liste de photos, mais il est un plus simple d'utiliser une boucle ordinaire sur le balisage d'une photo. Un modèle imbriqué aurait été justifié s'il s'agissait de données à changement dynamique et de réflexion automatique des changements dans le balisage (qui est un scénario pris en charge mais qui n'entre pas dans le cadre de cet article), mais vu qu'il s'agit de liaisons uniques et unidirectionnelles, le code en ligne fait parfaitement l'affaire.
function(__containerElement, $dataItem, $parentContext, __instanceId) {
   var __context = {}, $component, __app = Sys.Application, 
      __creatingComponents = __app.get_isCreatingComponents(), 
      __components = [], __componentIndex, __e, __f, __topElements = [],
      __p = [__containerElement], $index = __instanceId, 
      $id = Sys.Preview.UI.Template._getIdFunction(__instanceId), 
      $element = __containerElement;
   Sys.Preview.UI.Template._contexts.push(__topElements);
   with(__context) { with($dataItem || {}) {
      $element=__p[1]=document.createElement('LI');
      __topElements.push($element);
      $element=__p[2]=document.createElement('A');
      $component = $element;
      __e = document.createAttribute('href');
      __e.nodeValue = String.format('5_Client.aspx?product={0}',
                                                       ProductID);
      $element.setAttributeNode(__e);
      __p[1].appendChild($element);
      __p[2].appendChild(document.createTextNode(Name));
      $element=__p[2];
      __p[1].appendChild(document.createTextNode(" "));
      $element=__p[1];
   } 
}
   for (var __i = 0, __l = __topElements.length; __i < __l; __i++) {
      __containerElement.appendChild(__topElements[__i]);
   }
Sys.Preview.UI.Template._contexts.pop();
 return new Sys.Preview.UI.TemplateResult(this, __containerElement, __topElements, __components);
}
<div class="sys-template" id="productDetailsTemplate">
    <fieldset>
        <legend>{{ Name }} ({{ ProductNumber }}) 
            {{ String.format("{0:C}", ListPrice) }}</legend>
        <ul class="photoList">
            <!--* for (var i = 0; i < Photos.length; i++) { *-->
            <li><img src="{{ String.format('productphoto.ashx?id={0}',
                Photos[i]) }}" /></li>
            <!--* } *-->
        </ul>
        <table>
            <tr><td class="label">Summary:</td><td>{{ Summary }}</td></tr>
            <tr><td class="label">Experience:</td>
                <td>{{ RiderExperience }}</td></tr>
              ...
            <tr><td class="label">Style:</td><td>{{ Style }}</td></tr>
            <tr><td class="label">Wheel:</td><td>{{ Wheel }}</td></tr>
            <tr><td class="label">Maintenance:</td>
                <td>{{ MaintenanceDescription }}</td></tr>
        </table>
    </fieldset>
</div>
Les modèles sont compilés en différé la première fois qu'ils sont instanciés, mais ils sont préparés en créant un "new Sys.Preview.UI.Template" à l'aide de l'élément parent du balisage de modèle comme paramètre du constructeur. Les modèles eux-mêmes sont instanciés à partir de la publication de l'appel réseau qui rapporte les données du service Web sur le serveur :
AdventureWorks.GetProducts(1 /* Mountain bikes */,
  function(productArray) {
    renderProductList(productArray, productListTemplate);
    selectProduct(initialProductID, true);
});

function renderProductList(productArray) {
    var target = $get("productList");
    target.innerHTML = "";
    for (var i = 0, l = productArray.length; i < l; i++) {
        productListTemplate.createInstance(target, productArray[i]);
    }
}
Ceci ne sera pas nécessaire dans la version commerciale d'ASP.NET 4.0 ; son composant DataView s'occupera du traitement, de la compilation et de l'instanciation des modèles. L'essentiel du code de cette application finira par disparaître, mais il est utile en ce sens qu'il montre comment fonctionnent les choses en coulisses. Il montre également comment un développeur de composants qui souhaite inclure le rendu de modèle pourrait utiliser la fonctionnalité.

Propagation d'événements
Où dois-je mettre le code qui affiche la bonne vue de détails lorsqu'un utilisateur clique sur l'un des produits ? Le code, comme son équivalent côté serveur, utilise la propagation d'événements, si bien que j'ai pu écrire un gestionnaire d'événements à un seul clic pour tous les liens de la liste (ce qui me permettait d'y ajouter ou d'en supprimer des liens si je le souhaitais sans risquer de créer de nouveaux gestionnaires ou de supprimer des gestionnaires existants). Le code suivant montre ce gestionnaire. Tous les événements de clic des liens de la liste se propageront jusqu'à la liste elle-même, où ils seront traités. "e.target" est une référence à l'élément qui a fait l'objet d'un clic ; en d'autres termes, c'est le lien qui me permet de récupérer l'ID de produit de l'attribut href et de sélectionner le produit pertinent :
$addHandler($get("productList"), "click", function(e) {
    var href = e.target.href;
    selectProduct(parseInt(href.substring(href.indexOf('=') + 1), 10));
    e.preventDefault();
    e.stopPropagation();
});
Ensuite, l'action par défaut de l'événement (la navigation par liens) est annulée et l'événement ne peut plus se propager. Cette opération s'effectue en appelant les méthodes stopPropagation et preventDefault standard de W3C sur l'objet d'événement que l'infrastructure met à disposition sur tous les navigateurs, notamment Internet Explorer.

Gestion du bouton Précédent
La seule fonctionnalité que reste à reproduire par rapport à la version côté serveur est l'historique. L'activation de l'historique sur le contrôle ScriptManager active également des API côté client qui sont exactement identique aux API côté serveur que j'ai utilisées auparavant et qui peuvent même être utilisées en même temps (activation de la gestion d'état client-serveur mélangée).
La création de points d'historique se fait en appelant Sys.Application.addHistoryPoint à partir des événements qui correspondent à un changement d'état, en l'occurrence, en cliquant sur un produit de la liste :
Sys.Application.addHistoryPoint({product: productDetails.ProductID}, 
    "AdventureWorks - " + productDetails.Name);
De même, l'état est restauré depuis l'événement « navigate » sur Sys.Application. Les arguments HistoryEventArgs que le gestionnaire d'événements reçoit ont une propriété, state, qui vous permet de récupérer le produit à restaurer :
Sys.Application.add_navigate(function(sender, e) {
    var ProductID = parseInt(e.get_state()["product"], 10);
    selectProduct(ProductID, true);
});

Produit fini
La page qui en résulte se comporte de la même façon que la version UpdatePanel, mais il n'y a pas de comparaison possible sur le plan du trafic réseau. Lorsqu'un produit est sélectionné, la version UpdatePanel envoie plus de 4 Ko de données au serveur et en reçoit environ 8 Ko. La version AJAX pure, en revanche, envoie seulement "{"productId":771}" plus les en-têtes HTTP standard et reçoit 2 Ko de données JSON (JavaScript Object Notation). Cela revient à économiser environ 10 Ko à chaque fois qu'un utilisateur clique sur un produit.
Il ne s'agit là que d'une des nombreuses fonctionnalités passionnantes qui sont prévues pour ASP.NET 4.0. Partagez vos impressions sur go.microsoft.com/fwlink/?LinkId=126987.

Bertrand Le Roy, Ph.D., est le responsable de programme chargé d'AJAX chez Microsoft. Il a passé cinq ans comme développeur dans le même service. Il représente également Microsoft auprès de l'alliance OpenAjax.

Page view tracker