Comment exposer une application WCF RIA Services à d’autres clients : JSON endpoint (4/5)

Nous allons voir ici comment ouvrir l’application vue dans les 3 articles précédents avec un point d’accès de type JSON. Ce nouvel endpoint sera alors consommé par une application HTML5 faisant appel à jQuery pour appeler le service distant et donner un peu de vie à la page. Nous verrons ainsi comment afficher l’ensemble des livres de manière très dynamique grâce au plug-in Galleriffic, à l’utilisation d’un peu de CSS3 et de la balise <canvas>. Nous verrons ensuite comment ajouter ou supprimer une catégorie après avoir passé la barrière de l’authentification comme toujours. 

Cet article est le quatrième d’une série de 5 :

1 – Revue de l’application initiale
2 – Exposition du service sous la forme d’un flux OData consommé par Excel puis par une application écrite avec WebMatrix
3 – Exposition du service en WCF “classique” pour une consommation depuis WPF puis depuis Windows Phone 7
4 – Exposition du service en JSON pour une consommation par une application HTML5/jQuery (cet article) 
5 – Les étapes à suivre pour porter cette application vers Windows Azure et SQL Azure

Activation d’un endpoint JSON sur un DomainService WCF RIA Services

Comme pour l’ajout du endpoint SOAP vu dans l’article précédent, il vous faut installer le toolkit pour WCF RIA Services afin de pouvoir déclarer un endpoint JSON. Comme d’habitude, rendez-vos dans le web.config et sous le endpoint SOAP, ajoutez ce nouvel endpoint :

 <add name="JSON" type="Microsoft.ServiceModel.DomainServices.Hosting.JsonEndpointFactory,  
                       Microsoft.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral,  
                       PublicKeyToken=31bf3856ad364e35" />

Une fois cette opération effectuée, vous pourrez alors attaquer votre service avec une URL du type :

https://nomdevotreserveur:port/ClientBin/NomDeLaSolution-Web-NomDuDomainService.svc/JSON/GetEntities

Par exemple, dans mon cas, voici l’URL permettant de retourner l’ensemble des catégories en JSON :

https://bookclub.cloudapp.net/ClientBin/BookShelf-Web-Services-BookClubService.svc/JSON/GetCategories

Et voici le flux retourné :

 {"GetCategoriesResult":{"TotalCount":4,"RootResults":[{"CategoryID":2,"CategoryName":"Business"},
{"CategoryID":4,"CategoryName":"General"},{"CategoryID":1,"CategoryName":"Technology"},
{"CategoryID":3,"CategoryName":"Travel"}]}}

Gestion du endpoint depuis une application HTML5

La technique que nous allons voir ici fonctionnera avec tout type de technologie web : PHP, ASP.NET, JSP, etc. puisque nous allons mettre en place une application HTML5 basée sur jQuery fonctionnant uniquement côté navigateur.

Le principe consiste à aller attaquer les URLs où sont exposées nos méthodes de manipulation d’entités et de parser le JSON fourni en retour.

Pour cela, je suis parti des 2 articles suivants très utiles :

- WCF RIA Services, jQuery, and JSON endpoint - Part 1

- WCF RIA Services, jQuery, and JSON endpoint - Part 2

Par exemple, voici un morceau de script reposant sur jQuery permettant d’interroger mon service WCF RIA Services pour récupérer l’ensemble des catégories et les afficher dans le document HTML en cours :

 function LoadCategories() {
    var Params = {};
    Params.type = 'GET';
    Params.url = 'BookShelf-Web-Services-BookClubService.svc/JSON/GetCategories';
    Params.dataType = 'json';
    Params.success = function (data) {
        $('categories > p').remove();
        var returnedEntity;
        for (var i in data.GetCategoriesResult.RootResults) {
            returnedEntity = data.GetCategoriesResult.RootResults[i];
            $(categories).append('<p>' + returnedEntity.CategoryName + '</p>');
        }
    };

    $.ajax(Params);
}

Vous pouvez tester le résultat à travers cette page : https://bookclub.cloudapp.net/jQueryHome.html et vous obtiendrez ce rendu dans IE9 :

BookClubScreen027

J’utilise ici quelques nouveautés de CSS3 telles que les “border radius” (pour avoir les angles arrondis) ou le “box shadow” (pour avoir un effet d’ombre portée) mais la page se rendra bien quand même sous IE8 avec quelques effets en moins.

Très bien, nous savons désormais comment lire les données de notre couche RIA Services. Essayons alors de récupérer l’ensemble des informations concernant les livres présents en base et de mettre l’ensemble en forme en utilisant les dernières technologies du Web. Je me suis ainsi amusé à récupérer ces données et à les afficher dans une application HTML5 assez dynamique faisant appel à l’excellent plug-in Galleriffic. Avec quelques effets CSS3 comme vu au dessus et une animation utilisant la balise <canvas> en arrière-plan, cela donne ce genre de résultat :

BookClubScreen028

Vous pouvez tester cette application ici : https://bookclub.cloudapp.net/WCFRIA_jQueryHTML5.html

Une petite parenthèse sur HTML5…

Cette application fonctionne dans l’ensemble des navigateurs modernes supportant une partie de CSS3 (comme IE9, Chrome, Firefox, Opera ou Safari) ainsi que la balise <canvas> . Mais vous pouvez également la tester sur d’autres navigateurs plus anciens qui vous priveront juste à nouveau de certains effets. Cela est très important selon moi. On peut ainsi commencer à utiliser certaines nouveautés d’HTML5 et des technologies associées (CSS3, SVG) mais en faisant bien attention à ne pas utiliser des parties trop instables ou expérimentales (en gros tout ce qui est à base d’extensions propriétaires comme –webkit, –moz, –ms, –o, etc.) au risque d’être dépendant d’une implémentation particulière et de devoir revoir votre code fréquemment.

Je vous rappelle en effet qu’HTML5 est loin d’être prêt pour une utilisation complète en masse. En effet, les spécifications ne sont pas encore terminées ni suffisamment testées par des jeux de tests unitaires pour vous garantir un comportement commun sur tous les navigateurs. Par ailleurs, tant que les navigateurs modernes n’auront pas pris le dessus en terme de part de marché, vous prendrez forcément un risque à vous appuyer uniquement sur ces nouveautés en production.

Pour finir sur cette petite parenthèse, je trouve également qu’il est important à veiller de bien conserver une expérience agréable pour les navigateurs plus anciens. Pour cela, soit vous utilisez HTML5 pour améliorer l’expérience visuelle et utilisateur pour les navigateurs les plus récents. C’est l’approche que je conseille et que je préfère. Soit vous utilisez HTML5 comme cible principale et vous supportez les anciens navigateurs avec des sortes de “simulateurs”. Il existe par exemple des librairies JavaScript simulant la balise <canvas> pour les versions d’IE inférieures à 9 telle que canvasexplorer. Cependant, je trouve l’expérience à chaque fois catastrophique sous IE6/7/8. Vu l’importance des parts de marché de ces 3 versions combinées, vous prenez clairement le risque de vous mettre à dos les utilisateurs d’IE en préférant les punir de leur choix. Clignement d'œil

Dans mon cas, comme je préfère la 1ère approche, mon application fonctionne bien avec le moteur de rendu d’IE8/7 ou Quirks et donne ce résultat un peu moins sexy :

BookClubScreen028b

Mais néanmoins pleinement fonctionnel !

Analyse de l’application HTML5

Cette application repose donc en grande partie sur le plug-in Galleriffic. Ce dernier permet de créer et gérer pour vous les animations entre les éléments avec des effets gérés par jQuery (normalement des photos issues de Flickr dans les samples livrés par défaut) et l’affichage des vignettes sous la forme d’un bandeau en haut. Il permet également la navigation au clavier et l’affichage des différents éléments en mode “slideshow”. Pour terminer, il créé les ancrages dans les URLs pour permettre de référencer directement un livre particulier. Ainsi, sur l’exemple précédent, je suis positionné sur le livre “Data-Driven Services with Silverlight 2” de John Papa qui dispose du code ASIN suivant : 0596523092. Donc, l’URL directe vers ce livre est celle-ci : https://bookclub.cloudapp.net/WCFRIA_jQueryHTML5.html#0596523092 

Pour construire cette application, je suis parti du sample “example-5” fourni sur le site de Galleriffic et je génère dynamiquement dans le DOM ce type d’entrées dans ma liste :

 <li><a class="thumb" name="1419620037" href="/Content/Images/1419620037.jpg"
    title="7-Slide Solution(tm): Telling Your Business Story In 7 Slides or Less">
    <img src="/Content/Images/Thumbs/1419620037.jpg" 
         alt="7-Slide Solution(tm): Telling Your Business Story In 7 Slides or Less" />
</a>
    <div class="caption">
        <div class="image-title">
            7-Slide Solution(tm): Telling Your Business Story In 7 Slides or Less</div>
        <div class="book-title">
            Paul J. Kelly</div>
        <div class="image-desc">
            A unique approach to organizing and constructing business presentations that draws
            on the insights of cognitive psychology and provides an infrastructure to build
            presentations that resonate with your audience like a good story.</div>
    </div>
</li>

Elles sont paramétrées en fonction du retour JSON fait à la méthodes GetBooks() . Voici la fonction contenue dans le fichier WCFRIA_jQueryHTML5.js s’occupant de charger le flux JSON et de générer dynamiquement les bonnes entrées dans le DOM :

 function LoadBooks() {
    var Params = {};
    Params.type = 'GET';
    Params.url = 'BookShelf-Web-Services-BookClubService.svc/JSON/GetBooks';
    Params.dataType = 'json';
    Params.success = function (data) {
        var returnedEntity;
        var books = new Array();
        var newHtmlGenerated = new StringBuilder();

        for (var i in data.GetBooksResult.RootResults) {
            returnedEntity = data.GetBooksResult.RootResults[i];
            newHtmlGenerated.append('<li><a class="thumb" name="');
            newHtmlGenerated.append(returnedEntity.ASIN);
            newHtmlGenerated.append('" href="');
            newHtmlGenerated.append("/Content/Images/" + returnedEntity.ASIN + ".jpg");
            newHtmlGenerated.append('" title="');
            newHtmlGenerated.append(returnedEntity.Title);
            newHtmlGenerated.append('">');
            newHtmlGenerated.append('<img src="');
            newHtmlGenerated.append("/Content/Images/Thumbs/" + returnedEntity.ASIN + ".jpg");
            newHtmlGenerated.append('" alt="');
            newHtmlGenerated.append(returnedEntity.Title);
            newHtmlGenerated.append('" /></a>');
            newHtmlGenerated.append('<div class="caption">');
            newHtmlGenerated.append('<div class="image-title">');
            newHtmlGenerated.append(returnedEntity.Title);
            newHtmlGenerated.append('</div>');
            newHtmlGenerated.append('<div class="book-title">');
            newHtmlGenerated.append(returnedEntity.Author);
            newHtmlGenerated.append('</div>');
            newHtmlGenerated.append('<div class="image-desc">');
            newHtmlGenerated.append(returnedEntity.Description);
            newHtmlGenerated.append('</div></div></li>');
        }

        $('ul').append(newHtmlGenerated.ToString());

        LoadGalleriffic();
    };

    $.ajax(Params);
}

Sachant qu’ensuite je charge exactement la même logique que dans le sample “example-5” avec l’appel de la méthode LoadGalleriffic() . Par ailleurs, j’utilise ces petites fonctions pour pouvoir simuler un StringBuilder afin d’être plus efficace qu’une brutale concaténation de chaines à base de += :

 function StringBuilder() {
    this.buffer = [];
}

StringBuilder.prototype.append = function append(string) {
    this.buffer.push(string);
    return this;
};

StringBuilder.prototype.ToString = function ToString() {
    return this.buffer.join("");
};

Cette approche permet une consommation mémoire moindre et surtout des performances nettement supérieures lorsque l’on manipule des grosses chaines de caractères en JavaScript (et d’une manière plus générale d’ailleurs) et ce, quelque soit le navigateur utilisé. Je vous conseille d’ailleurs la lecture de cet article autour de ce sujet : 43,439 reasons to use append() correctly

Pour l’animation en fond, j’utilise donc la balise <canvas> de HTML5. Cette dernière nous permet d’avoir une zone de type bitmap dans laquelle on va écrire à l’aide de primitives appelées en JavaScript. Mon objectif était d’avoir un effet à la PlayStation (comme le fond animé sur une PS3) en arrière-plan. En cherchant, je me suis aperçu qu’il existait déjà un tutorial proposant de faire quelque chose de similaire : Tutoriel Canvas : Réaliser une bannière animée en quelques lignes de code (je ne suis d’ailleurs pas du tout d’accord avec l’auteur sur le fait que canvas remplacera Flash ou Silverlight !). Dans mon cas, je voulais la même animation mais placée en fond et derrière ma zone principale d’affichage des livres. Pour cela, je me suis inspiré de la technique décrite ici : Animated website background with HTML5 . Pour finir, il faut tester le support de <canvas> pour éviter une erreur de script sous IE6/7/8. Cela se fait à travers ce simple morceau de script :

 function isCanvasSupported(){  
     var elem = document.createElement('canvas');  
     return !!(elem.getContext && elem.getContext('2d'));
}

Que j’ai trouvé en lisant ce thread sur Stackoverflow : Best way to detect that HTML5 <canvas> is not supported

Pour finir, vous noterez en observant le source des 2 pages précédentes que j’ai essayé de suivre une méthodologie que je trouve particulièrement intéressante nommée “Unobtrusive JavaScript”. Cela permet de dissocier clairement le rôle de chacun des éléments constituant une page web : le markup pour le contenu de la page, le CSS pour l’habillage et le script pour donner vie à l’ensemble. jQuery se prête d’ailleurs très bien à cet exercice grâce à son sélecteur puissant qui permet d’attacher toute la logique aux éléments HTML dans un fichier séparé “après coup” si j’oserais dire. On retrouve ainsi une hygiène bienvenue que les développeurs ASP.NET connaissent déjà parfaitement où le markup de la présentation est séparée du code-behind. 

Note : Vous noterez aussi sur les copies d’écran que les icônes des boutons “Back” et “Forward” sont de couleurs différentes que les couleurs par défaut. C’est parce que j’ai utilisé les nouvelles possibilités d’IE9 permettant d’épingler les sites à la barre de tâches de Windows 7 :

BookClubScreen031

Vous trouverez de la documentation à ce sujet ici : https://msdn.microsoft.com/library/gg491738(v=VS.85).aspx . Je me suis donc créé un icône en utilisant l’application HTML5 suivante : https://ie.microsoft.com/testdrive/Browser/IconEditor/Default.html

Puis j’ai simplement ajouté ce morceau de HTML pour référencer le résultat :

 <link rel="shortcut icon" href="/favicon.ico"/>

Passer l’authentification pour les opérations d’écriture

Comme d’habitude, et comme vu dans les articles précédents, il faut être authentifié et membre du groupe “Admin” pour pouvoir manipuler les entités exposées par notre couche WCF RIA Services. Dans le cas de nos clients WPF et Windows Phone 7 accédant à travers le endpoint SOAP, cela demandait un peu de travail. En effet, il fallait s’occuper de gérer le cookie d’authentification dans les requêtes HTTP qui partaient vers les méthodes gérant l’écriture (ajout, suppression, mise à jour). L’avantage d’avoir ici une application tournant dans le navigateur est que cette opération va être nettement plus simple. Il va juste falloir appeler la méthode Login() du DomainService chargée de l’authentification. Une fois le login réussi, le navigateur renverra pour nous le cookie magique vers le 2ème DomainService.

Pour pouvoir s’authentifier, j’ai inclus la logique nécessaire dans cet page : https://bookclub.cloudapp.net/jQueryLogin.html et plus particulièrement dans le fichier jQueryLogin.js qui présente le code suivant :

 function Auth() {
    var _username = $(UserName).val();
    var _password = $(Password).val();
    var loginParameters = JSON.stringify({ 'userName': _username, 'password': _password, 
                                           'isPersistent': 'true', 'customData': '' });
    var result;

    //Create jQuery ajax request
    var Params = {}
    Params.type = "POST";
    Params.url = 'BookShelf-Web-AuthenticationService.svc/JSON/Login';
    Params.dataType = "json";
    Params.data = loginParameters;
    Params.contentType = "application/json"
    Params.success = function (data) {
        for (i in data.LoginResult.RootResults) {
            result = data.LoginResult.RootResults[i];
            var ul = $('<p><img src=' + result.PictureURL + ' width=64/> Welcome ' 
                     + result.DisplayName + '</p>');
            $(ul).appendTo($('#UserLogged'));
        }
        if (result != null) {
            $("#AccountInfo").fadeOut(500, null);
        }
        else {
            $("#AccountInfo").append("<p><font color='red'>Error while logging. Check username or password.</font></p>");
        }
    }

    //Make the ajax request
    $.ajax(Params);
}

On fait donc un appel vers la méthode Login() du endpoint JSON du service AuthenticationService.svc exposé par défaut dans toutes les applications de type “Silverlight Business Application”. Si l’authentification a réussie, on fait disparaitre avec un effet de fadeOut() la zone contenant nos contrôles de saisie puis on ajoute un message de bienvenu à l’utilisateur tout en récupérant sa photo de profile générée avec la webcam de l’application Silverlight principale.

Voici un exemple avant login puis après login :

BookClubScreen029BookClubScreen030

Une fois logué, on peut désormais faire des appels vers les méthodes d’écriture. La couche WCF RIA Services s’occupera de vérifier que vous êtes bien dans le bon rôle précisé par attribut avant de vous laisser toucher aux entités.

Si vous faites un appel à l’une de ses méthodes sans être dans le bon rôle ou sans être authentifié, voici le type de réponse que vous aurez au SubmitChanges() :

 {"ErrorCode":401,
"ErrorMessage":"You must be part of the Administrator role to insert, update or delete a category.",
"IsDomainException":false,
"StackTrace":"   at System.ServiceModel.DomainServices.Server.DomainService.
ValidateMethodPermissions(DomainOperationEntry domainOperationEntry, …"}

Le message d’erreur précisé par attribut remonte donc bien également sur les appels JSON. Pour finir, voici un exemple de script permettant d’ajouter une nouvelle catégorie via le endpoint JSON et affichant le message d’erreur ci-dessus en cas de problème de droits :

 var newCategoryIdInserted;

function InsertNewCategory() {
    var _categoryName = $(CategoryName).val();
    var changeSet = [];

    var entityChange = {};
    //Setting Id for entityChange, not the entity key but how the changeset tracks each change if there is more than one
    entityChange.Id = 0;
    entityChange.Entity = { '__type': "Category:#BookShelf.Web.Models", 'CategoryName': _categoryName };

    //Set the type of Operation to be performed on the Entity
    //2 - insert
    //3 - update
    //4 - delete
    entityChange.Operation = 2;

    changeSet.push(entityChange);

    var changsetPayload = JSON.stringify({ "changeSet": changeSet });

    //Create jQuery ajax request
    var Params = {}
    Params.type = "POST";
    Params.url = 'BookShelf-Web-Services-BookClubService.svc/JSON/SubmitChanges';
    Params.dataType = "json";
    Params.data = changsetPayload;
    Params.contentType = "application/json";
    Params.error = function (httpRequest, textStatus, errorThrown) {
        var error = JSON.parse(httpRequest.responseText, null);
        alert(error.ErrorMessage);
    }
    Params.success = function (data) {
        for (i in data.SubmitChangesResult) {
            result = data.SubmitChangesResult[i];
            // keeping a copy of the returned affected CategoryID
            // for further operations (update/delete)
            newCategoryIdInserted = result.Entity.CategoryID;
            alert("Category " + _categoryName + " added successfully with ID: " + newCategoryIdInserted);
        }
    }

    //Make the ajax request
    $.ajax(Params);
}

Et voici le code pour supprimer cette nouvelle catégorie :

 function DeleteCategory() {
    var changeSet = [];

    var entityChange = {};
    entityChange.Id = 0;
    entityChange.Entity = { '__type': "Category:#BookShelf.Web.Models", 'CategoryID': newCategoryIdInserted };

    //Delete
    entityChange.Operation = 4;
    changeSet.push(entityChange);

    var changsetPayload = JSON.stringify({ "changeSet": changeSet });

    //Create jQuery ajax request
    var Params = {}
    Params.type = "POST";
    Params.url = 'BookShelf-Web-Services-BookClubService.svc/JSON/SubmitChanges';
    Params.dataType = "json";
    Params.data = changsetPayload;
    Params.contentType = "application/json";
    Params.error = function (httpRequest, textStatus, errorThrown) {
        var error = JSON.parse(httpRequest.responseText, null);
        alert(error.ErrorMessage);
    }
    Params.success = function (data) {
        for (i in data.SubmitChangesResult) {
            result = data.SubmitChangesResult[i];
            alert("CategoryID: " + result.Entity.CategoryID + " deleted successfully.");
        }
    }

    //Make the ajax request
    $.ajax(Params);
}

Vous trouverez ces 2 méthodes dans le fichier jQueryAdmin.js lui-même utilisé par la page jQueryAdmin.html que vous pouvez tester ici : https://bookclub.cloudapp.net/jQueryAdmin.html . Vous aurez alors ce genre de résultat. Tentative d’insertion ou suppression sans être logué :

BookClubScreen032

Insertion après être logué en tant qu’admin :

BookClubScreen033BookClubScreen034

Suppression :

BookClubScreen035

Vous pouvez télécharger le code source de la solution Visual Studio 2010 regroupant ces 4 articles ici :

Voilà, cela conclue cet article. J’avoue m’être particulièrement bien amusé sur l’écriture de l’application HTML5. Cependant, je trouve la productivité, l’outillage et la qualité de l’ensemble de la chaine de production bien inférieure à celle que nous avons lorsque nous développons des applications riches en Silverlight. Le passage d’un environnement riche à base de C#, XAML sous Visual Studio et Expression Blend à du JavaScript, HTML, CSS  sous l’équivalent de Notepad reste particulièrement douloureux. Silverlight est donc capable de faire ce que fait HTML5 dans sa globalité (ou plutôt fera dans quelques années) bien plus vite, donc moins cher, avec une qualité de code supérieure et un rendu cross-navigateurs assuré par le plug-in. Par ailleurs, Silverlight propose un ensemble important de fonctionnalités qui ne seront pas envisagées avant HTML6… Cela va de choses anodines comme la gestion de la Webcam au support du DRM sur la vidéo. Si le sujet vous intéresse, Bernard Ourghanlian, notre directeur technique à Microsoft France, a écrit un livre blanc sur le sujet : Silverlight et HTML5 . Il résume bien aujourd’hui les différences entre ces 2 mondes.

Et je ne parle pas des nouveautés que nous avons annoncé pour Silverlight 5 autour de la 3D ou des appels de type P/Invoke ! Par contre, bien sûr, Silverlight étant un plug-in propriétaire, il ne pourra pas non plus satisfaire tout le monde et couvrir toutes les plateformes.

HTML5 a donc un potentiel intéressant et mérite d’être suivi par les développeurs web dans les prochaines années. Il ne doit donc surtout pas être négligé. Microsoft est d’ailleurs pleinement engagé sur les spécifications au W3C, la création de jeux de tests unitaires et le support d’HTML5 à travers notre navigateur Internet Explorer 9. Mais HTML5 et Silverlight ne sont pas des ennemis. Ils sont complémentaires et visent des scénarios différents.

Le mieux étant quand même de savoir travailler avec les 2. Vous vous rendrez alors compte facilement des forces et des faiblesses de ces 2 offres pour le développement d’applications RIA du futur. Vous serez également à même d’avoir une vision plus juste que les débats stériles du genre “HTML5 va tuer machin” souvent lancés par des personnes ne maitrisant malheureusement aucun des 2 environnements et alimentant ces débats sans aucune base technique solide.  

Allez, à bientôt pour le 5ème et dernier article consacré à la migration du code dans Azure !

David