Préambule
Après s'être frotté au couple Prototype / Scriptaculous (à l'époque un exemple d'autocompleter avec Scriptaculous), on va s'intéresser à la fameuse librairie jQuery.
Concernant ASP.NET Ajax, je n'y ai personnellement jamais adhéré : trop opaque, trop compliqué, trop peu performant, et l'UpdatePanel non merci lorsque l'on voit ce qu'il transporte, et aussi certainement un manque de motivation de ma part pour cette librairie. A la rigueur, je pourrais dans l'absolu utiliser des composants du projet AjaxControlToolkit, certains sont assez bien vus.
Mais revenons à jQuery, quels avantages peut-on trouver à jQuery :
- une forte communauté, et pour un projet opensource, cela reste primordiale,
- le 1er point induit que le projet est très actif, et évolue constamment, ce qui peut rassurer (correction de bugs, performances, réponses aux besoins...),
- la sphère jQuery est décliné en 3 domaines : la librairie, les plugins, les interfaces utilisateurs (widgets, effets, interfaces améliorées), c'est propre et livré au format compressé,
- le chaînage des fonctions (s'assimile à la fluent interface) ,
- la puissance des selectors
Parmi les extensions, on va regarder du côté de l'autocomplete, qui nous servira pour de l'aide à la saisie.
Le code côté serveur reste du C#, mais on pourra facilement le transcrire vers n'importe quel langage : Ruby (http://json.rubyforge.org ou sous merb), PHP (http://fr.php.net/json), ...
Recherche de postes
On se propose un exercice simple : avoir une zone de saisie, dans laquelle on recherchera des libellés de postes : ingénieur, chef, ... l'objectif est de proposer une liste d'intitulés contenant la chaîne de caractères saisie : un autocompleter (mais quelle idée originale Olivier tu nous as encore trouvé ici !).
On va décliner l'exercice en 2 versions : une basique où une liste de mots sera affiché, et une autre, avec un retour en JSON, où l'affichage des propositions et l'élément choisi sera un peu différent.
De quoi aura-t-on besoin ?
- d'un handler (dit .ashx) pour la recherche Ajax : le plus simple et le plus efficace (on pourra lire cet intéressant benchmark pour s'en laisser convaincre),
- dans ce script, d'un dictionnaire d'intitulés de poste, avec pour chacun le niveau nécessaire :
var lpostes = new Dictionary<string, string>(); lpostes["Secrétaire"] = "0"; lpostes["Assistant(e)"] = "+1"; lpostes["Ingénieur d'études et développement"] = "+4|+5"; lpostes["Ingénieur informaticien (générique)"] = "+5"; lpostes["Chef de projet"] = "+4"; lpostes["Ingénieur Système"] = "+5"; lpostes["Web designer"] = "+3"; lpostes["Chargé de mission"] = "+3|+4|+5"; lpostes["Chef de service"] = "-1";
- d'un sérializer JSON,
- de la mécanique javascript avec jQuery côté page,
- et enfin de l'extension autocomplete qui se charge d'envoyer en méthode GET les paramètres suivants au script : q pour les mots recherchés, limit pour la limite de retour et d'affichage des résultats (par défaut 10), et un timestamp du moment de l'exécution de la recherche. L'extension contient principalement une méthode d'extension autocomplete() (rendu possible grâce à jQuery et à la fonction $.fn.extend()), qu'il suffit d'appeler sur un objet de type input.
Les parties communes aux 2 versions
Au niveau de la page Web contenant la zone de recherche, on trouvera les styles qu'il faudra ajuster pour ces propres besoins, ainsi que la partie javascript pour les 2 versions.
On utilise la version Google de la librairie. Pour l'autocomplete on téléchargera les fichiers idoines Autocomplete et BGIframe (optionnel); ce dernier permet de corriger des défauts d'IE 6.0.
La partie XHTML et CSS - autocomplete a besoin d'un ensemble de classes, elles commencent toutes par ac_ :
ac_results : la fenêtre de propositions, ac_loading : l'image de spinner ou d'attente (on pourra s'en créer un ici), ac_even / ac_odd : le fond des éléments affichés (couleur éléments pairs / impairs), ac_over : l'élément courant dans la fenêtre en surbrillance)
Le code XHTML + CSS:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head> <script src="http://www.google.com/jsapi" type="text/javascript"></script> <script type="text/javascript"> google.load("jquery", "1.3.2"); </script> <script type="text/javascript" src="http://dev.jquery.com/view/trunk/plugins/autocomplete/lib/jquery.bgiframe.min.js"></script> <script type="text/javascript" src="http://dev.jquery.com/view/trunk/plugins/autocomplete/jquery.autocomplete.js"></script> <style type="text/css"> .ac_results { padding: 0px; border: 1px solid black; background-color: white; overflow: hidden; z-index: 99999; } .ac_results ul { width: 100%; list-style-position: outside; list-style: none; padding: 0; margin: 0; } .ac_results li { margin: 0px; padding: 2px 5px; cursor: default; display: block; /* if width will be 100% horizontal scrollbar will apear when scroll mode will be used */ /*width: 100%;*/ font: menu; font-size: 12px; /* it is very important, if line-height not setted or setted in relative units scroll will be broken in firefox */ line-height: 16px; overflow: hidden; } .ac_loading { background: white url('spinner.gif') right center no-repeat; } .ac_even { background-color: #D2D6DC; } .ac_odd { background-color: #eee; } .ac_over { background-color: #FFFFBB; color: black; } </style> <title>Test autocomplete</title> </head> <body> <form action="search.ashx"> recherche basique : <input type="text" id="TBIntitule1" style='width: 350px'/><br /> recherche JSON : <input type="text" id="TBIntitule2" style='width: 350px'/> </form> <!-- ici le javascript --> </body> </html>
la partie Javascript en fin de page :
<script type="text/javascript"> $().ready(function() { // retour simple $("#TBIntitule1").autocomplete("search.ashx", { minChars: 3, width: 250, selectFirst: true, extraParams: { simple: true } }); // retour JSON $("#TBIntitule2").autocomplete("search.ashx", { minChars: 3, width: 350, dataType: "json", selectFirst: true, formatItem: function(data, i, max, value, term) { return value; }, parse: function(data) { var mytab = new Array(); for (var i = 0; i < data.length; i++) { var myres = data[i].Libelle; var myvalue = data[i].Libelle + ' (' + data[i].Niveau + ')'; mytab[mytab.length] = { data: data[i], value: myvalue, result: myres }; } return mytab; } }); });
ou une autre version pour le parse avec la fonction map :
parse: function(data) { return $.map(data, function(row) { return { data: row, value: row.Libelle + ' ('+row.Niveau+')', result: row.Libelle } });
Explication des différents paramètres :
- minChars : le nombre de caractères pour lequel l'appel Ajax se déclenchera, ici à partir de 3 caractères saisis, la recherche s'effectuera,
- width : largeur de la zone des propositions retournées par le script,
- selectFirst : si true, le 1er élément de la liste sera sélectionné lors de la validation par Tab ou Return,
- extraParams : paramètres supplémentaires à passer au script Ajax (on aura ici par exemple search.ashx?q=mots&limit=10&simple=true),
- dataType : format des données à traiter en retour,
- parse : surcharge la fonction existante (par défaut découpe les données de retour avec \n comme séparateur entre les lignes, et | pour chaque champ d'un enregistrement) : permet de traiter les données spécifiquement, ici, l'objet JSON ramené. Retourne un tableau contenant l'enregistrement courant (data[i]), la valeur (value) à afficher dans la fenêtre de proposition, et la valeur effective (result) à mettre dans le champ de saisie après sélection,
- formatItem : handler qui sera appelé pour chaque élément ramené par le script, en paramètres : la ligne courante, l'indice de la ligne courante, la valeur courante, le(s) mot(s) clé(s) de recherche
Version basique
Le script Ajax appelé par l'autocomplete doit ramener la liste des éléments sous forme d'une liste avec comme séparateur le retour à la ligne (\n) pour chacun des mots. L'extension se chargera ensuite de mettre en forme les résultats sous forme de <ul> <li>valeur</li>...</ul>
on aura la requête Linq suivante pour la recherche dans la liste : recherche de chaîne, résultats ordonnancés, et prendre les max (tiré du paramètres limit de l'appel Ajax) 1ers résultats - on crée un type anonyme pour avoir la structure Libelle - Niveau :
var r = (from p in lpostes where p.Key.IndexOf(strSearch, StringComparison.InvariantCultureIgnoreCase) > -1 orderby p.Key select new {Libelle = p.Key, Niveau = p.Value}).Take(max);
puis sa mise en forme pour le retour, liste des libellés, séparés par un retour à la ligne :
var results = string.Join(Environment.NewLine, r.Select(x => x.Libelle).ToArray()); context.Response.ContentType = "text/plain"; context.Response.Write(results);
Version JSON
A l'heure actuelle, le format d'échange le plus répandu entre systèmes reste XML. En revanche lorsqu'il s'agit d'effectuer des traitements côté navigateur (FF, IE, Safari, Opéra), cela devient difficile de trouver le moyen de le réaliser : traitements lents et lourds. Avec l'arrivée d'Ajax, il est devenu nécessaire, voire primordiale, de trouver un moyen efficace d'échange de données entre le serveur et le navigateur, JSON est alors devenu le format de facto. Une fois une expression JSON évaluée, celle-ci devient un objet javascript sur lequel on peut effectuer des opérations : gain de temps et de performances assurés, et simplicité au rendez-vous !
Au niveau de l'interface Web, concernant les propositions, on affichera l'intitulé avec son niveau requis, ensuite la sélection d'un choix remplira uniquement l'intitulé sélection dans la zone de saisie.
Cela donnera ceci pour la saisie de 'ing' :
cela ramènera la chaîne suivante :
[{"Libelle":"Ingénieur d\u0027études et développement","Niveau":"+4|+5"},{"Libelle":"Ingénieur informaticien (générique)","Niveau":"+5"},{"Libelle":"Ingénieur Système","Niveau":"+5"}]
Le code de recherche Linq est identique à la version basique, seul le retour diffère. On utilise dans ce cas des méthodes d'extensions C# pour sérialiser facilement notre résultat de requête Linq (un IEnumerable d'un type anonyme) en JSON.
// sérialise le type anonyme Libelle = val1, Niveau = val2, cela donnera un tableau du type // ["Libelle":"Ingénieur informaticien (générique)","Niveau":"+5", "Libelle":"Ingénieur d'études et développent","Niveau":"+4|+5","Libelle":"Ingénieur Système","Niveau":"+5"] var json = r.toJSON(); context.Response.ContentType = "application/json"; context.Response.Write(json);
Remarque : à l'origine je souhaitais utiliser la classe DataContractJsonSerializer, que je pensais la classe "officielle" (WCF). Apparemment, pas tout à fait, JavaScriptSerializer qui devait être obsolète à partir de la v. 3.5 SP1, ne parait plus tout à fait l'être (en tout cas, sous VStudio, ce n'est pas indiqué), et cette dernière seule permet de sérialiser des types anonymes, DataContractJsonSerializer nous rendant une exception.
Mais on me dit à l'oreillette (j'ai Scott en direct en studio à L.A.) que tout compte fait la classe JavaScriptSerializer n'est plus obsolète (undeprecated) dans le SP1 (et là je me dis pourquoi avoir 2 classes pour faire presque la même chose...le presque a son importance, bref). Apparemment, je ne suis pas le seul à m'être posé la question.
La classe helper fortement inspirée pour ne pas dire pompée de chez ScottGu :
public static class JSONHelper { /// <summary> /// Sérialise un objet en une chaine JSON /// </summary> /// <param name="myobject"></param> /// <returns></returns> public static string toJSON(this object myobject) { var myjson = new JavaScriptSerializer(); return myjson.Serialize(myobject); } /// <summary> /// Désérialise une chaine JSON en un objet T /// </summary> /// <typeparam name="T"></typeparam> /// <param name="mystrjson"></param> /// <returns></returns> public static T fromJSON<T>(this string mystrjson) { var myjson = new JavaScriptSerializer(); return myjson.Deserialize<T>(mystrjson); } }
Version complète du handler search.ashx
Remarque : si le handler ne doit pas être public (ie : un site demandant une authentification), on peut rajouter l'interface IRequiresSessionState, qui déclenchera une demande de Session, afin de sécuriser son accès le cas échéant.
/// <summary> /// Handler de recherche Ajax /// </summary> [WebService(Namespace = "http://dummy.net/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class search : IHttpHandler { public void ProcessRequest(HttpContext context) { var strSearch = context.Request["q"] ?? string.Empty; var max = int.Parse(context.Request["limit"] ?? "20"); var simple = bool.Parse(context.Request["simple"] ?? "false"); var lpostes = new Dictionary<string, string>(); lpostes["Secrétaire"] = "0"; lpostes["Assistant(e)"] = "+1"; lpostes["Ingénieur d'études et développent"] = "+4|+5"; lpostes["Ingénieur informaticien"] = "+5"; lpostes["Chef de projet"] = "+4"; lpostes["Web designer"] = "+3"; lpostes["Chargé de mission"] = "+3|+4|+5"; lpostes["Chef de service"] = "-1"; var r = (from p in lpostes where p.Key.IndexOf(strSearch, StringComparison.InvariantCultureIgnoreCase) > -1 orderby p.Key select new {Libelle = p.Key, Niveau = p.Value}).Take(max); switch(simple) { case true : var results = string.Join(Environment.NewLine, r.Select(x => x.Libelle).ToArray()); context.Response.ContentType = "text/plain"; context.Response.Write(results); break; default : var json = r.toJSON(); context.Response.ContentType = "application/json"; context.Response.Write(json); break; } } public bool IsReusable { get { return false; } } }
Conclusion
Simple, efficace, ce type d'extension peut rendre de bien beaux services, mangez-en.
En ASP.NET Webforms, on veillera à encapsuler tout le code nécessaire à la gestion de l'autocomplete dans un UserControl, il sera ainsi facilement utilisable dans toute page.
Ressources
Le code source est en annexe.
La documentation officielle de jQuery ou encore une interface pour rechercher dans la documentation avec snippets à l'appui ou encore ce site en français.
Toutes les options disponibles pour l'extension autocomplete.
La démo de l'extension et aussi un exemple avec json.