Dans le dernier tutorial, on a donné un peu de style à notre application Windows 8 Metro en passant par Microsoft Blend for Visual Studio:
Windows Metro – Clouder! (4) – Donner du style avec Microsoft Blend
Pour l’instant, on a tout codé dans la fonction « ready », notre appel XHR au service SoundCloud et la manipulation de notre vue. Bien sûr, ce ne sera pas le dernier appel au service SoundCloud que l’on va réaliser, on va devoir récupérer des titres selon certains critères (prévus dans l’API SoundCloud), des commentaires, des informations utilisateur, …
Pour éviter de se retrouver avec trop de code / logique dans chaque « page », on va centraliser ces appels XHR dans un objet « SoundCloudService » qui servira uniquement à créer des appels XHR vers SoundCloud.
Création d’objets, de namespaces, notions de « classes »
Pour créer un objet SoundCloudService, vous avez pas mal d’options. Vous avez tout d’abord les méthodes WinJS.Namespace.define et WinJS.Class.define:
Windows Metro – Les fonctions WinJS.Namespace.define et WinJS.Class.define
En gros, WinJS.Namespace.define vous permet de garnir un Namespace avec des propriétés (hors prototype). WinJS.Class.define vous rappellera un peu le système de classe en programmation orientée objet (JavaScript n’a pas de notion de « classes » mais simplement des objets, toujours dynamiques). Vous pouvez créer des « classes » en ajoutant des fonctions (méthodes) et des variables (propriétés) au prototype de l’objet.
Ces méthodes sont simplement des raccourcis vers un fonctionnel JavaScript « classique » du web. A noter une petite spécificité, si vous utilisez ces méthodes, WinJS va marquer toutes les fonctions comme « supportedForProcessing », plus d’informations ici:
Il y a aussi une autre technique que vous verrez dans de nombreux exemples de chez Microsoft. La technique est d’isoler votre code dans une « self executing anonymous function » qui se définit comme ceci:
(function () { "use strict"; // vars ... // fonctions ... })();
Tout le code qui se trouve dans cette fonction ne sera pas visible de l’extérieur, et c’est le but. Sauf qu’un objet complètement opaque depuis l’extérieur, cela ne sert pas à grand chose. Vous allez donc pouvoir lui déclarer une « API publique », c’est-à-dire des fonctions qui seront visibles depuis l’extérieur de cette fonction.
Cela se fait par un appel à WinJS.Namespace.define, par exemple:
(function () { "use strict"; function someProtectedFunction(){ // whatnot }; function someNotExposedFunction(){ // can't see me }; WinJS.Namespace.define("SomeNamespace", { somePublicAPIFunction: someProtectedFunction }); })();
Vous pourrez ensuite accéder à la fonction par SomeNamespace.somePublicAPIFunction, qui sera une sorte de lien symbolique vers someProtectedFunction. Vous ne pourrez en revanche pas accéder à « someNotExposedFunction depuis l’extérieur car son scope est celui de la fonction.
Si vous n’avez pas tout compris, je vous conseille tout de même de réviser un peu votre JavaScript, cela vous aidera à comprendre les exemples en ligne:
http://bonsaiden.github.com/JavaScript-Garden/
Création de l’objet SoundCloudService
Maintenant que j’ai présenté plusieurs technique, laquelle va-t-on utiliser? Et bien aucune de celles-là, on va la faire « à l’ancienne ». Même si cela parait plus verbeux, on va créer un objet SoundCloudService en définissant son prototype et l’instancier puis on va assigner l’instance dans un namespace applicatif.
La raison de ce choix est assez personnelle, même si les approches proposées par WinJS (ou pas d’autres librairies) semblent plus sexy, au final j’arrive plus à lire un code simple basé sur prototype. Libre à vous de faire différemment (la version originale de Clouder est par exemple créée à partir de la technique des self-executing functions, expliquée plus haut.
On va commencer par créer un fichier pour notre objet SoundCloudService, qui va se nommer SoundCloudService.js et se placer dans /js/ dans notre projet. Add > New item > JavaScript file.
A noter que vous ne pouvez pas renommer des fichiers dans Visual Studio quand vous êtes en debug. C’est un peu ridicule mais c’est comme ça ;).
La première étape est d’ajouter ce fichier dans notre application. Pour cela, on va le mettre dans default.html, avec un tag script:
<!-- ClouderTutorial references --> <link href="/css/default.css" rel="stylesheet" /> <script src="/js/default.js"></script> <script src="/js/navigator.js"></script> <script src="/js/SoundCloudService.js"></script>
Notre projet étant assez trivial, on ne va pas créer de namespace applicatif pour le définition notre objet SoundCloudService (app.service.SoundCloudService par exemple) et on va l’appeler directement SoundCloudService.
Voici le contenu de SoundCloudService.js, repris depuis le code des articles précédents:
/** * @constructor */ SoundCloudService = function () { }; // "static" properties SoundCloudService.CLIENT_ID = "client_id=2e601fddcc4a07954c3da7077f81a1e1"; SoundCloudService.API_BASE_URL = "https://api.soundcloud.com/"; // "methods" SoundCloudService.prototype.getTracks = function (options, success, error) { var url = SoundCloudService.API_BASE_URL + "tracks.json?" + SoundCloudService.CLIENT_ID; // error callback passed as second parameter // http://msdn.microsoft.com/en-us/library/windows/apps/hh868282.aspx WinJS.xhr({ url: url }) .done(function (request) { var response = request.responseText; var data = null; try { data = JSON.parse(response); } catch (e) { console.log("SoundCloudService::getTracks Something wrong happened : " + response); if (error) { error(); } return; } var items = []; for (var i = 0, l = data.length; i < l; i++) { items.push(data[i]); } success(items); }, error); };
On a donc un objet que l’on va instancier et placer à la racine de notre application, directement sur window. On va faire cela à l’initialisation de l’application, dans default.js:
app.addEventListener("activated", function (args) { if (args.detail.kind === activation.ActivationKind.launch) { if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) { window.soundCloudService = new SoundCloudService(); } else { // TODO: This application has been reactivated from suspension. // Restore application state here. } .....
On a donc maintenant une instance de SoundCloudService nommée « soundCloudService » sur window. Allons donc remplacer notre code dans /pages/home/home.js:
(function () { "use strict"; WinJS.UI.Pages.define("/pages/home/home.html", { onHottestTracks : function(data){ var list = new WinJS.Binding.List(data); var listView = document.querySelector(".tracklist"); WinJS.UI.setOptions(listView.winControl, { layout: new WinJS.UI.GridLayout(), itemDataSource: list.dataSource }); }, ready: function (element, options) { window.soundCloudService.getTracks({}, this.onHottestTracks); } }); })();
Relancez l’application, vous devriez avoir le même résultat qu’avant. Enfin presque, j’ai oublié l’option « order » dans la requête.
Envoi des options de filtrage / tri à l’API SoundCloud
Dans le tutorial précédent, on avait ajouté « order=hotness » dans la requête pour dire à SoundCloud ne pas trier les résultats par date de création. L’API SoundCloud dispose de nombreux autres options de filtrage ou de tri:
http://developers.soundcloud.com/docs/api/reference#tracks
Au lieu de prévoir tous ces paramètres dans la signature de notre méthode « getTracks », on va lui passer un objet contenant toutes les options. Il est déjà prévu dans le code ci-dessus, mais non exploité. Il suffit donc de boucler sur le propriétés de cet objet et de les passer en GET dans la requête sans oublier d’encoder les valeurs comme URI:
SoundCloudService.prototype.getTracks = function (options, success, error) { var url = SoundCloudService.API_BASE_URL + "tracks.json?" + SoundCloudService.CLIENT_ID; for (var key in options) { var value = options[key]; url += "&" + key + "=" + encodeURIComponent(value); }
Pour trier par « hotness », on peut simplement passer l’option dans l’appel:
window.soundCloudService.getTracks({order:"hotness"}, this.onHottestTracks);
Pour passer une recherche, on pourrait donc faire:
window.soundCloudService.getTracks({q: "some text", order:"hotness"}, this.onHottestTracks);
Pré-traitement sur les objets « tracks provenenant de SoundCloud
Pour l’instant, on prend les objets bruts, tels qu’ils viennent de SoundCloud. Mais si vous regardez la qualités des photos, elle ne sont pas très clean (pixellisées) et certaines n’ont pas de photo. On peut demander à SoundCloud, une photo de meilleure qualité, c’est indiqué dans la documentation:
Idem pour le nombre de commentaire qui est « undefined » quand les commentaires sont désactivés sur le morceau. Pour éviter ce genre de situations, on va faire un petit traitement sur les objets avant des rebalancer dans la callback. Pour cela, on va créer une fonction « formatTrack » dans SoundCloudService.js qui va prendre un item et redéfinir ses propriétés:
SoundCloudService.formatTrack = function (item) { if (!item.artwork_url || item.artwork_url == "") { if (item.user && item.user.avatar_url && item.user.avatar_url.indexOf("default_avatar_") == -1) { // replace the artwork_url by the user's avatar if no track artwork is defined item.artwork_url = item.user.avatar_url; } else if (item.waveform_url) { // if the user has no avatar, fallback on the waveform_url item.artwork_url = item.waveform_url; } else if (item.kind == "playlist" && item.tracks && item.tracks.length > 0) { // if the item is a playlist, get the artwork_url of the first track item.artwork_url = item.tracks[0].artwork_url; } } // get the HD version of the picture if (item.artwork_url) { item.artwork_url = item.artwork_url.replace("large", "crop"); } // adds a second field, concat the username and the genres var subtitle = item.user.username; if (item.genre && item.genre != "") { subtitle += " in " + item.genre; } item.subtitle = subtitle; // non-commentable if (!item.comment_count) { item.comment_count = "-"; } if (!item.playback_count) { item.playback_count = "-"; } if (!item.favoritings_count) { item.favoritings_count = "-"; } // append the CLIENT_ID to the track audio stream so that it can be played straight away. item.stream_url = item.stream_url + "?" + SoundCloudService.CLIENT_ID; return item; };
Puis on process toutes les tracks qui reviennent du service:
for (var i = 0, l = data.length; i < l; i++) { items.push(SoundCloudService.formatTrack(data[i])); } success(items);
Et voilà le résultat, sans image manquante:
Conclusion
Dans cette 5ème partie, on a remit un peu d’ordre dans notre code, en créant un objet pour s’occuper des appels API SoundCloud. On a aussi ajouté un prétraitement sur les objets provenant de l’API pour éviter les valeurs nulles et obtenir des images de meilleure qualité.