TechDays 2010 : Migration de MOSS 2007 à SharePoint Server 2010

sharepoint-server

Mardi 9 février à 11H lors des TechDays 2010, avec mon collègue Antoine Dongois, j'aurai le plaisir de co-animer la session "Quel plan de migration prévoir de SharePoint 2007 à SharePoint 2010"

Nous y décrirons les différents chemins de migration possibles avec leurs avantages et leurs inconvénients, et ferons une démo de migration de type DB Attach de bout en bout.

Rendez-vous aux TechDays!

SharePoint 2010 : Utiliser le modèle objet dans un exécutable

sharepoint-server

 

Lorsqu’on essaie pour la première fois d’utiliser le modèle objet de SharePoint 2010 dans une application console ou autre exécutable .NET classique, l’aventure peut commencer par l’erreur FileNotFoundException suivante :

error2

 

Ce problème est normal : par défaut, Visual Studio configure la plupart des projets pour une plateforme x86. Mais pour utiliser le modèle objet de SharePoint 2010, un projet devra être configuré pour une plateforme x64.

Cela se configure en quelques clics. Voici comment faire :

 

solution-explorer

Dans le Solution Explorer, un clic-droit sur le nœud de la solution, puis Configuration Manager.

 

configuration-manager

On crée ensuite une nouvelle “Platform” via le bouton New…

 

configuration-manager2

La fenêtre propose alors de créer une plateforme “Any CPU” (compatible x64), on peut valider sans rien changer.

 

configuration-manager3

Le projet est maintenant correctement configuré, relançons l’application pour tester.

 

success

L’objet SPSite s’instancie maintenant correctement.

 

Ce problème n’est pas seulement lié à la classe SPSite. En pratique, la première fois que j’ai eu ce problème, c’était avec un appel à SPFarm.Local qui renvoyait null si le projet est configuré en x86.

Selon le type d’objet que l’on cherche à manipuler, on aura donc soit une référence nulle, soit une erreur lors de l’instanciation.

Dans les deux cas, rien n’indique au développeur que le problème est lié à la configuration de plateforme du projet. Il devra donc en avoir conscience pour exploiter le modèle objet dans un projet “non-SharePoint”.

Heureusement, les nouveautés de SharePoint 2010 ainsi que les nouvelles possibilités offertes par Visual Studio 2010 pour le développeur SharePoint feront vite oublier ce petit désagrément :)

 

Arnault Nouvel

SharePoint 2007 : jQuery.SPItemsRotator pour afficher des éléments de listes avec animation

 

Avoir des sites “qui bougent” est une demande récurrente des utilisateurs qui s’occupent de la mise en place de sites SharePoint. Il est vrai qu’à l’heure du web 2.0, on s’attend à voir de belles animations dans nos navigateurs, même dans un intranet.

Si on souhaite avoir des éléments de liste qui apparaissent les uns après les autres avec un fondu, on peut procéder de différentes manières :

  • Avec une web part avec code serveur + javascript, ce qui implique le déploiement d’un package sur le serveur
  • Avec une DataView web part + javascript, ce qui n’est pas toujours simple à mettre en place, surtout pour un utilisateur final
  • Avec du javascript uniquement, avec accès aux données par web service. Ce n’est pas optimal d’un point de vue performances, mais permet une intégration rapide si la solution est générique et clé-en-main. 
  • Je passe volontairement sur l’option Silverlight qui est encore plus contraignante dans SharePoint 2007

Le plug-in jQuery.SPItemsRotator correspond à la 3ème option. Il s’intègre en quelques minutes et permet d’afficher les éléments d’une liste d’annonce standard ou d’une bibliothèque d’images standard, avec des fondus entre chaque élément ou image. Ceci ne requiert pas le déploiement de package sur la ferme, tout se fait avec SharePoint designer. Attention cependant, pour les sites publics internet il est vaut mieux choisir les options 1 ou 2 pour des raisons de performances et de sécurité.

jQuery.SPItemsRotator s’appuie sur jQuery et jQuery.SPServices, un plug-in jQuery qui propose une API pour les web services SharePoint. Il est très utile et bien pensé, je ne peux que vous encourager à y jeter un œil.

Après avoir référencé les 3 fichiers JavaScript dans la master page du site à l’aide de SharePoint Designer, on peut placer des jParts (le terme à la mode) pour obtenir quelque-chose comme ceci :

Les jParts sont en pratique des Content Editor Web Parts qui contiennent du code HTML et jQuery :

<!-- Web Part de gauche -->
<div id="divAnnouncements" style="height:150px;"></style>
<script>
   $(window).ready(function(){                                
           $("#divAnnouncements").rotateAnnouncements({
            webURL:"/sites/spitemsrotator", //url du site
            listName:"My Announcements" //nom de la liste
      });
   });
</script>
<!-- Web Part de droite -->
<div id="divPictures" style="height:200px;"></style>
<script>
   $(window).ready(function(){                                
         $("#divPictures").rotatePictures({
            webURL:"/sites/spitemsrotator",
            listName:"My Pictures",
            fadeDuration:1500, //durée du fading en millisecondes
            displayDuration:5000 //durée d'affichage de chaque élément en millisecondes
      });
   });
</script>

Voilà pour l’utilisation basique des méthodes rotatePictures et rotateAnnouncements.

Celles-ci définisse une requête CAML et un formatage précis puis appellent rotateItems, la méthode principale. Un développeur ou un utilisateur avancé peut aussi appeler cette méthode pour afficher et filtrer les éléments en spécifiant la requête CAML, et les formater comme il veut en décidant du pattern de rendu.

Pour exemple prenons le code de rotateAnnouncements, qui peut servir de source d’inspiration pour écrire d’autres rotateXXXXX() méthodes :

jQuery.fn.rotateAnnouncements = function(options){

    var opts = $.extend({
        displayDuration:10000,
        fadeDuration:1500,
        CAMLViewFields:"<ViewFields><FieldRef Name='Body' /></ViewFields>",
        CAMLQuery:"<Query><Where><Or><IsNull><FieldRef Name='Expires' /></IsNull><Geq><FieldRef Name='Expires' /><Value Type='DateTime'><Today /></Value></Geq></Or></Where></Query>",    
        itemHtml:"{ows_Body}"
    }, options);

    return this.rotateItems(opts);
    
}

Un dernier avantage, il est aussi facile de placer la zone de rotation dans une page que dans la master page : au lieu d’utiliser une Content Editor Web Part, on peut placer le HTML+script directement dans la master page avec SharePoint Designer.

Bonnes rotations :)

SharePoint 2007 : Nouvelle version de WSPCompare

Je me suis récemment surpris à utiliser WSPCompare uniquement pour regarder le contenu d’un package .wsp. Suite à ce constat, j’ai publié une nouvelle version de WSPCompare qui dispose d’une interface plus appropriée à ce type d’utilisation.

L’outil propose donc maintenant 2 modes d’utilisation.

Mode navigation si 1 package est ouvert

Browse

Mode comparaison si 2 packages sont ouverts

Compare

 

Je me suis aussi amusé à réaliser une petite vidéo de présentation de l’outil :

 

Pour télécharger WSPCompare, c’est par ici : http://wspcompare.codeplex.com


Classé sous , , ,

SharePoint 2007 : WSPCompare

Il arrive parfois d’avoir besoin de comparer 2 versions d’un package .wsp pour SharePoint, par exemple après le refactoring d’une solution, ou lors de la mise en place d’un build TFS.

L’objectif est en général de vérifier qu’aucun fichier n’a été oublié, et que le manifest.xml est cohérent.

J’ai donc développé WSPCompare, un outil permettant de comparer le contenu de 2 solutions SharePoint. Celui-ci est comme d’habitude publié sur CodePlex.

Après avoir sélectionné les 2 fichiers .wsp à comparer, l’outil présente la liste de tous les fichiers trouvés, en utilisant un code couleur pour identifier les différences :

WSPCompareSummary

L’onglet Détails permet de visualiser le contenu des fichiers pour pouvoir les comparer :

WSPCompareDetails

Pour les téléchargements, c’est par ici : http://wspcompare.codeplex.com/Release/ProjectReleases.aspx


Classé sous , , ,

SharePoint 2007 : Consommer un web service avec jQuery

 

Background

Je suis récemment intervenu sur un projet SharePoint dans une très grande entreprise. Pour faire court, dans cette entreprise l'équipe intranet en charge de la ferme SharePoint fournit une collection de sites aux équipes métier qui en font la demande. De par l'architecture de SharePoint, l'équipe métier est alors autonome pour gérer sa collection de sites.

Une de ces équipes m'a sollicité pour les aider à la mise en place de leur projet de portail. La collection de sites comprend un sous-site visible par tout les utilisateurs, et un sous-site par pays (chaque utilisateur a accès au site de son pays, + quelques autres pour les responsables par continent) dont l’administration serait déléguée à une équipe dans chaque pays. Mon client m'a donc demandé de développer une page d'accueil dynamique qui proposerait des liens pour chacun des sous-sites accessibles par l'utilisateur courant, avec une mise en forme classique : titre, logo et la description du sous-site.

Sans hésitation, j’explique qu’il suffit de développer une web part dédiée qui utiliserait le modèle objet de SharePoint pour identifier les sous-sites auxquels l’utilisateur a accès, et qu’il y en aurait au grand maximum pour une journée. En effet, dans le cadre de mon intervention de 5 jours, le temps de développement se devait d’être court.

Mon client valide l’idée et je contacte l’équipe intranet pour vérifier que le déploiement d’un package .wsp ne poserait pas de problème. Manque de chance, c’était sans compter sur la longueur de la procédure de validation, en particulier durant cette période des vacances. Nous sommes vite arrivés à la conclusion qu’il était risqué de miser sur un déploiement en production en moins de 5 jours : 

  • Rédaction d’un cahier des charges “officiel”
  • Réunion avec l’équipe intranet pour discuter du cahier des charges et de l’impact du développement sur la ferme
  • Développement et packaging .wsp de la web part en question
  • Revue de code par l’équipe intranet
  • Déploiement en environnement de recette
  • Tests + validation par l’équipe intranet
  • Tests + validation par l’équipe métier (avec la possibilité d’un retour à la case départ)
  • Déploiement en production (nocturne)

Tout cela est 100% justifié : il faut garder à l’esprit qu’un bug ou une faille dans un développement SharePoint, aussi insignifiant soit-il, peut mettre en péril la stabilité de la ferme SharePoint, et l’intégrité des données de la majeure partie de l’intranet. De plus cette procédure devrait être répétée à chaque fois qu’une modification du composant serait nécessaire dans le futur. Le jeu en valait-il vraiment la chandelle ?

Non, surtout qu’en appelant les web services SharePoint par JavaScript, on peut obtenir un résultat équivalent. C’est une approche moins classique, mais qui permet de gagner du temps sur le packaging, le déploiement et la maintenance du composant. L’appel au web service sera grandement facilité par l’utilisation de jQuery, le framework JavaScript à la mode qui dispose d’une méthode dédiée facile d’utilisation, et surtout cross-browser.

 

Implémentation

Avant d’aller plus loin il fallait valider qu’un web service pouvait me fournir les informations dont j’avais besoin. Le SDK de SharePoint 2007 détaille chaque web service disponible : http://msdn.microsoft.com/en-us/library/ms445760.aspx

Webs.asmx dispose d’une méthode GetWebCollection qui retourne la liste des sous-sites du site courant avec leurs titres et url, et GetWeb.asmx retourne le titre, le code langue et la description d’un site donné grâce à l’url passé en paramètre. Bingo !

J’ai référencé dans la master page du site les fichiers JavaScript (celui de jQuery et le mien) placés dans la bibliothèque “Styles Library”, et inséré sur la page d’accueil une web part de contenu éditable contenant :

  • une balise (qui accueillera le menu)
  • une ligne de JavaScript pour exécuter ma méthode de remplissage de cette balise.

CEWP

Pour l’édition du fichier .js, on peut se servir de SharePoint Designer pour naviguer facilement jusqu’à l’emplacement de notre fichier javascript et de le modifier directement, tout en bénéficiant d’une reconnaissance syntaxique du JavaScript.

Ensuite, sur une machine virtuelle MOSS 2007, on peut récupérer l’enveloppe soap correspondante à la méthode que l’on souhaite appeler à l’aide de son navigateur. En l’occurrence, j’ai saisi l’url “http://localhost/_vti_bin/Webs.asmx” dans mon navigateur, et cliqué sur le lien de la méthode GetWebCollection :

Definition

On peut alors écrire la partie JavaScript. La méthode FillMenu() appellera le web service en utilisant l’enveloppe SOAP que l’on vient d’obtenir et remplira le menu. Voici le contenu du fichier JavaScript :

   1:  
   2: var wsWebsUrl;
   3:  
   4: function FillMenu(){
   5:     
   6:     wsWebsUrl = GetWebServiceUrl("Webs.asmx");
   7:     
   8:     if(wsWebsUrl != null){
   9:         
  10:         var request = " \
  11:        \
  12:          \
  13:        \
  14:     ";
  15:       
  16:         $.ajax({
  17:             url: wsWebsUrl,
  18:             type: "POST",
  19:             dataType: "xml",
  20:             data: request,
  21:             success: FillMenu_GetWebCollectionHandler,
  22:             contentType: "text/xml; charset=\"utf-8\"",
  23:             async:true
  24:         });
  25:         
  26:     }
  27: }
  28:  
  29: function FillMenu_GetWebCollectionHandler(response){
  30:     
  31:     var menuRows = "";
  32:  
  33:       // Pour chaque sous élément Web
  34:       $(response).find("Web").each(function() {
  35:          
  36:          var webTitle = $(this).attr("Title");
  37:          
  38:          if(webTitle != "Group"){ //On ne souhaite pas proposer de lien vers Group
  39:              
  40:              var webUrl = $(this).attr("Url");
  41:              
  42:              //récupération des informations relatives au site cible
  43:              var webInfos = FillMenu_GetWebInfos(webUrl)    
  44:              if(webInfos != null){
  45:  
  46:                 var webDescription = $(webInfos).attr("Description");
  47:                 var webLogo = webUrl + "/images/logo.png"; //les logos sont stockée dans chaque site
  48:         
  49:                 // On ajoute une entrée au menu
  50:                 menuRows += " \
  51:                                  \
  52:                                     \
  53:                                          \
  54:                                      \
  55:                                  \
  56:                                  \
  57:                                     " + webTitle + " \
  58:                                     
" + webDescription + "
\
  59:                                  \
  60:                             ";
  61:             }
  62:         }
  63:     });
  64:     
  65:     //on cache, on remplit, et on fait apparaitre le menu avec une animation
  66:     $("#testWS").hide().append(""
+ menuRows + "").slideDown();
  67:  
  68: }
  69:  
  70: function FillMenu_GetWebInfos(webUrl){
  71:     
  72:     var requestWebs = " \
  73:        \
  74:          \
  75:        \
  76:     ";
  77:  
  78:     var returnValue = null;
  79:     
  80:     //appel synchrone qui ne réussit que si l'utilisateur a le droit d'accès au site cible
  81:     $.ajax({
  82:         url: webUrl + "/_vti_bin/Webs.asmx",
  83:         type: "POST",
  84:         dataType: "xml",
  85:         data: requestWebs,      
  86:         contentType: "text/xml; charset=\"utf-8\"",
  87:         async:false,
  88:         success:function(response){
  89:  
  90:                     //construction de la requete soap avec insertion de l'url du site en paramètre
  91:                     var requestWeb = " \
  92:                                        \
  93:                                          \
  94:                                           " + webUrl + " \
  95:                                          \
  96:                                        \
  97:                                     ";
  98:             
  99:                     //appel synchrone pour récupérer les informations du site cible
 100:                     $.ajax({
 101:                         url: wsWebsUrl,
 102:                         type: "POST",
 103:                         dataType: "xml",
 104:                         data: requestWeb,      
 105:                         contentType: "text/xml; charset=\"utf-8\"",
 106:                         success:function(response){
 107:  
 108:                                     returnValue = $(response).find("Web");
 109:                                 },
 110:                         async:false
 111:                     });
 112:  
 113:                 }
 114:         
 115:     });
 116:     
 117:     return returnValue;
 118:  
 119: }
 120:  
 121: // Récupération de l'url du web service dans le contexte du site courant
 122: function GetWebServiceUrl(filename){
 123:         
 124:     //    inspiré par SharePoint Bookmarklets : http://imtech.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=19047
 125:     var UrlPart={Url:0,ServerRelativeUrl:1,Site:2,SiteCollection:3};
 126:     var urlRegex=/^https?:\/\/[^\/]+(((\/(?:sites\/[^\/]+\/)?).*?)((lists\/([^\/]+)|pages|_catalogs|_layouts|_wpresources|_vti_bin|([^\/]+)\/forms|[^\/]+\.aspx).*)?)$/i;
 127:     var url=window.location.href.toLowerCase();
 128:     var match=urlRegex.exec(url);
 129:     
 130:     if( match == null)
 131:         return null
 132:     else
 133:         return match[UrlPart.Site] + "_vti_bin/" + filename;
 134:         
 135: }

On notera l’utilisation d’un appel asynchrone au web service pour la récupération des sous-sites et d’un appel synchrone pour la récupération de la description d’un site. Cet appel synchrone était préférable dans mon cas pour pouvoir remplir ma grille au fur et à mesure.

Voici le résultat :

Capture

Edit : Un appel à GetWebCollection sur chaque sous-site est nécessaire pour déterminer si l’utilisateur courant y a accès.Le security-trimming n’est pas aussi efficace que je le pensais. Le code a été modifié en fonction.

Conclusion

Au final, j’ai gagné un temps fou. Certes, le menu apparait quelques dixièmes de secondes après que la page soit chargée, mais j’ai pu effectuer ce développement sans déployer de code sur la ferme. La maintenance est facilitée, et les risques d’impacts négatifs sur les serveurs sont nuls : Au pire des cas, l’utilisateur aura une erreur JavaScript.

Les web services SharePoint permettent d’obtenir la plupart des informations dont on a besoin lors d’un développement classique, de manipuler les listes, leurs éléments (insertion, modification, suppression), et bien plus encore. Il y a même un web service pour faire de la correction orthographique!

Après avoir vu la vidéo sneak peak de SharePoint 2010 pour les développeurs, en particulier la démo sur l’intégration d’applications Silverlight, on devine que les web services qui composent le “modèle objet client” seront enrichis et améliorés (WCF). On constate que la stratégie de Microsoft est de limiter l’impact d’un développement sur une ferme SharePoint, et de donner plus d’autonomie aux développeurs en réduisant les interactions nécessaires avec les administrateurs de ferme. Les développeurs pourront déployer eux-même leurs applications Silverlight sans risque. En effet, si un développement ne comporte pas de code serveur et qu’il n’interagit avec les données que via des web services qui prennent en compte la sécurité, il n’y a aucun risque pour la santé de la ferme et l’intégrité de ses données. SharePoint pourra alors devenir la plateforme de choix pour toutes les applications intranet, et le coeur du système d’information de l’entreprise.

Il est déjà possible dans SharePoint 2007 d’héberger des applications Silverlight, mais cela nécessite l’intervention d’un administrateur de ferme pour effectuer les modifications nécessaires sur la ferme.

JavaScript est à mon sens l’équivalent dans SharePoint 2007 de ce que sera Silverlight dans SharePoint 2010 en terme de facilité d’intégration. Alors autant s’y mettre tout de suite car les développements 100% client utilisant les web services pour manipuler les données, c’est l’avenir !


Classé sous , ,

MOSS 2007 : jQuery picture viewer Web Part

Récemment, un client m’a demandé de lui développer une web part qui afficherait une bibliothèque d’images sous forme de miniatures, permettant de les télécharger, et de les visualiser en taille réelle via un click de souris.

Ces spécifications m’ont rappelé une superbe démonstration d’Aurélien Verla sur jQuery lors de sa présentation Découverte des nouveautés d'ASP.NET 4.0 aux TechDays 2009.

J’ai donc souhaité intégrer cela dans MOSS 2007, en utilisant la puissance de la Content by Query Web Part (CQWP) afin de bénéficier de fonctionnalités de tri et de filtrage sur les images affichées. Le tout est bien sûr réalisable entièrement dans SharePoint Designer, mais l’envie m’a pris d’aller un peu plus loin que les spécifications initiales et, pourquoi pas, publier ce projet sur CodePlex.

Lien : MOSS 2007 jQuery picture viewer Web Part

Capture

 

Pour avoir la possibilité de télécharger les images, au lieu de les ouvrir simplement dans Internet Explorer, la web part réutilise la page /_layouts/download.aspx, qui existe nativement dans SharePoint. Elle permet de télécharger un fichier dont l’url est passé dans la query string.

Exemple : http://office12-server/sites/t1/_layouts/download.aspx?sourceUrl=http://office12-server/sites/t1/Picture%20Library/Tree.jpg

Cette url renvoie le fichier Tree.jpg de la bibliothèque d’image “Picture Library” située dans la collection de sites http://office12-server/Sites/t1

 

Comme dit plus haut, il est possible de faire la même chose sans déployer de package sur la ferme. Ce développement est ainsi packagé afin de rendre la taille des miniatures paramétrable, de laisser la possibilité de désactiver les animations jQuery, et de pouvoir utiliser 2 fois la web part sur une même page avec un paramétrage différent.

Un développeur MOSS pourrait donc télécharger les sources et utiliser les fichiers .xsl, .js et .css pour intégrer la web part avec SharePoint Designer. Cela ne nécessiterait ni le déploiement de package sur le serveur, ni l’accord de l’administrateur de la ferme :)
A peu de choses près, il suffirait de modifier le fichier JavaScript afin qu’il s’initialise automatiquement, sans appel généré par du code serveur.

SharePoint 2007 : WSPDownload

Après BlobCacheAdmin, je vous propose aujourd’hui une nouvelle page d’administration pour l’administration centrale de SharePoint.

Inspirée par le post de Kype, cette page permet aux administrateurs de télécharger les solutions packages .wsp présents dans le solution store d’une ferme SharePoint.

Après installation de WSPDownload.wsp, un nouveau lien apparaitra dans l’onglet Operations de l’administration centrale :

WSPDownloadLink WSPDownloadForm 

Vous pourrez alors cliquer sur un des solution packages pour le télécharger.

Cet outil est disponible au téléchargement sur CodePlex dans le projet SharePoint Administrator Packages : http://spadminpack.codeplex.com

SharePoint 2007 : BlobCacheAdmin - Page d’administration du Blob Cache

Le Blob Cache permet d’utiliser du cache disque dans les sites SharePoint, ce qui évite des aller-retours avec la base de données pour la récupération de fichiers images, videos, ou autres. Il repose sur une ligne de configuration qu’il fallait jusqu’ici modifier à la main dans le fichier Web.Config de chaque serveur frontal, avec le risque d’erreur que cela induit.

J’ai développé une page d’administration pour permettre à un administrateur de définir ce paramétrage directement dans la Central Administration de SharePoint. Celle-ci utilise la classe SPWebConfigModification, afin que tous les serveurs frontaux soient mis à jour automatiquement. Comme l’explique Sébastien dans son post Gestion des Applications Web SharePoint et sites IIS : Best Practices, cela permet aussi que de nouveaux serveurs ajoutés à la ferme soient mis à jour automatiquement.

Disponible sur CodePlex, BlobCacheAdmin.wsp met à disposition une nouvelle page dans la Central Administration :

Lien dans la page Application Management
BlobCacheAdminLink

Page d’administration du Blob Cache
BlobCacheAdmin

Ce package est le premier de mon nouveau projet CodePlex, SharePoint Administrator Packages, qui réunira plusieurs outils d’administrations.

SharePoint : Optimiser le temps de développement et packager avec WSPBuilder

Ce post a pour but de présenter la manière dont je travaille au quotidien sur mes projets SharePoint, qui s'appuie sur l'outil WSPBuilder (la version en ligne de commande).
Il fait suite à un projet de démonstration que j'ai réalisé avec Visual Studio 2005 dans le cadre d'une formation sur le développement SharePoint. Il contient quelques features et peut servir de modèle lors du démarrage d'un projet SharePoint. Je le propose donc au téléchargement :
DemoWSP.zip

Un projet SharePoint qui se respecte doit  être livré sous la forme d'un SharePoint Solution Package, afin de pouvoir être déployé sur une ferme SharePoint par son administrateur.
Si on attend la fin du développement avant de s'intéresser au packaging, on risque d'aboutir à un refactoring couteux en temps, une solution en forme de plat de spaghettis, et une facture d'aspirines salée.
Alors que si on pense dès le début du projet à la problématique du packaging, la solution sera plus propre, et on gagnera en temps de développement et de maintenance. C’est là qu’intervient la magie de WSPBuilder, outil open source permettant d'automatiser le packaging d'un développement SharePoint.

L'avantage principal de WSPBuilder par rapport aux extensions Visual Studio pour WSS (qui de manière transparente génèrent un package et le déploient à chaque fois qu’on presse F5) est que le développeur exerce un réel contrôle sur la structure et le contenu de son package. De plus, ce modèle permet le déploiement de fichiers dans des répertoires non supportés par les extensions visual studio pour WSS: 12\Resources, 12\TEMPLATE\LAYOUTS, 12\TEMPLATE\IMAGES, etc.

Bien que WSPBuilder soit indépendant de Visual Studio (dans sa version en ligne de commande), la manière la plus pratique de l'utiliser est de créer un projet vide dans Visual Studio (qu'on appellera projet de packaging) dans lequel on créé 3 répertoires :
- un répertoire "12" qui aura la même structure de sous répertoire que celle du répertoire 12 de SharePoint,
- un répertoire "GAC" dans lequel iront les assemblii à déployer dans le GAC,
- et un répertoire "80" dans lequel iront les fichiers à déployer dans les répertoires des web applications.

Chacun de ces répertoires est optionnel, mais dans la majorité des cas on aura besoin des répertoires "12" et "GAC". Il suffit alors d'exécuter WSPBuilder.exe dans le répertoire du projet de packaging pour générer le package .wsp.
Note : Il est conseillé de renommer le package en .cab et de vérifier son contenu, car il peut arriver que WSPBuilder ne produise pas le résultat attendu. Il faut alors utiliser la méthode traditionnelle : création manuelle du .ddf et manifest.xml. Cela m'est arrivé une seule fois, c'est donc très rare, d’ailleurs je pense que le problème a été corrigé dans une des dernières versions de l’outil.

De par la simplicité d'utilisation de WSPBuilder, il n'est pas rare de trouver en entreprise des projets qui l'exécutent directement dans le build-event du projet.
Cela a deux avantages :
- après chaque compilation, un package à jour est généré et il suffit de le déployer via STSADM pour le tester.
- Si on travaille avec Team System, Il suffit dans le team build d'indiquer la solution cible pour que le package .wsp soit généré à chaque compilation, sans paramétrage supplémentaire. Ca, c’est génial.

Malheureusement, en phase de développement il y a de sérieux désavantages :
- Chaque compilation déclenche une génération du package ce qui prend beaucoup plus de temps
- Il faut ensuite déployer le package via STSADM ce qui prend aussi pas mal de temps

Je vais présenter ici la manière dont je travaille, qui permet d'optimiser le temps d'attente entre le moment de la compilation du projet et le moment où on en apprécie le résultat dans son navigateur web, tout en bénéficiant des avantages de WSPBuilder.
L'idée est de faire un déploiement "manuel" en phase de développement pour minimiser le temps d'attente, et de n'utiliser WSPBuilder que lors d'une compilation en mode Release (pour les team builds, et de temps en temps pendant la phase de développement pour ne pas avoir à modifier le fichier web.config de la web application à la main).
Ce modèle requiert un temps de préparation pour organiser la solution et créer les scripts au début du projet, mais le gain de productivité pour la suite est appréciable. Les temps d'attentes entre les déploiements et les tests seront réduit au strict minimum.
 

Solution


La solution Visual Studio comprendra 3 projets de type class library :

- DemoWSP : Le projet de packaging qui servira de structure de répertoires conforme à ce qu'attend WSPBuilder. Il contient les fichiers à déployer dans répertoire 12 (features, resources, etc.), un répertoire GAC et un répertoire 80.
- DemoGAC qui produit une assembly signée (contenant le code applicatif), vouée à être déployée dans le GAC.
- Demo80 qui produit une assembly non signée (contenant le code des web parts), vouée à être déployée dans le répertoire bin de la web application. 

Afin de s'assurer que notre projet pourra être compilé sur un serveur TFS, on prévoit un répertoire Lib dans le répertoire de notre solution, on y place les assemblii SharePoint, et on les référence dans nos projets à partir de ce répertoire Lib.
On devra aussi installer WSPBuilder sur le serveur TFS dans le même répertoire que sur nos machines virtuelles de développement (ou le mettre lui aussi dans le répertoire Lib).

A gauche, une capture d’écran de la structure de ma solution.

 

Le projet de packaging dispose d'un build-event qui appelle CreatePackage.bat lors d'une compilation en mode Release :

BuildEvents

En phase de développement, j'utilise deux scripts situés à la racine du projet de packaging:
- QuickDeploy.bat qui effectue un déploiement complet, copiant chaque fichier au bon endroit et recyclant ma pool d'application. Il met environ 1 seconde à s'exécuter, énorme gain comparé à la méthode consistant à générer puis déployer le package.
- HiveDeploy.bat qui copie uniquement les fichiers du répertoire 12, ne nécessitant pas le recyclage de ma pool d'application. L’exécution étant instantanée, ce script est très utile lorsque l'on effectue une modification sur une page .aspx par exemple, on peut la tester immédiatement car l’application n’a pas besoin d'être redémarrée.

HiveDeploy.bat
Ici, on tire le meilleur parti de WSPBuilder car on profite du fait que la structure de répertoires attendue correspond exactement à celle du répertoire 12. Il suffit de copier notre répertoire 12 dans celui de SharePoint.

@echo off
@SET SPDIR="c:\program files\common files\microsoft shared\web server extensions\12"

cls

ECHO.
Echo ================= Copying files to 12 directory ====================
ECHO.
xcopy /e /y /q 12\* %SPDIR%
ECHO.

QuickDeploy.bat
De même,  on copie notre 12 dans le 12 de SharePoint, ensuite on installe chaque assembly dans son répertoire cible. Enfin, on recycle le pool d'application que l'on utilise pour nos tests.

@echo off
@SET SPDIR="c:\program files\common files\microsoft shared\web server extensions\12"
@SET GACUTIL="C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe"

cls

ECHO.
Echo ================= Copying files to 12 directory ====================
ECHO.
xcopy /e /y /q 12\* %SPDIR%

ECHO.
Echo =================== Installing assemblies to GAC ===================
ECHO.
%GACUTIL% /uf "DemoGAC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=53439e9aa159f9fe" /silent
%GACUTIL% -if ..\DemoGAC\bin\debug\DemoGAC.dll
ECHO.

echo ========== Installing assemblies in Web Application's bin ==========
ECHO.
xcopy /e /y /q ..\Demo80\bin\debug\Demo80.dll "C:\Inetpub\wwwroot\wss\VirtualDirectories\80\bin\"
ECHO.

Echo ================== Recycling Application Pools =====================
ECHO.
cscript C:\WINDOWS\System32\iisapp.vbs /a "SharePoint - 80" /r
ECHO.

Ce qui donne a l’écran :

QuickDeploy


CreatePackage.bat
On récupère les assemblii provenant de nos 2 autres projets, puis on exécute WSPBuilder afin que le package .wsp soit généré.

@echo off
@SET WSPBUILDER="M:\Utilitaires\WSPBuilder\WSPBuilder.exe"
cls

ECHO ================== Updating solution directory =====================
ECHO.
REM deleting bin directory to make sure WSPBuilder won't include unnecessary assemblies in the package
rd .\bin /s /q
copy ..\Demo80\bin\release\Demo80.dll .\80\bin\
copy ..\DemoGAC\bin\release\DemoGAC.dll .\GAC\

ECHO.
ECHO ================== Building package with WSPBuilder ===================
%WSPBUILDER% -WSPName DemoWSPBuilder.wsp -DLLReferencePath ..\Lib\
ECHO.


Pour s’assurer que les assemblii soient compilés avant l’appel à CreatePackage.bat, il faut aussi faire attention à ce que le projet de packaging soit "compilé" en dernier en paramétrant le build-order de la solution. Pour ce faire, on déclare une dépendance de DemoWSP sur les 2 autres projets :

BuildOrder1   Dependencies

 

Remarques :
- WSPBuilder génère automatiquement toutes les policies et balises SafeControl nécessaires, un autre avantage de l’outil. Lorsque c’est nécessaire, pendant la phase de développement, on peut donc déployer le package généré pour éviter d’avoir à faire ces modifications à la main dans le fichier web.config.
- Ce modèle de travail peut être étendu aux solutions utilisant le pattern MVP, moyennant quelques modifications des scripts .bat.

SharePoint 2007 : Comment utiliser la page d’attente standard

Parfois lors d’un gros traitement, un retour serveur peut durer plusieurs secondes. Il est alors d’usage d’informer l’utilisateur qu’un traitement est en cours, comme le fait SharePoint pour toutes les opérations longues, telles que la création d’un site par exemple.

L’objet de ce post, c’est que la page utilisée par SharePoint est prévue pour être utilisée dans vos développements WSS ! Son utilisation est d’ailleurs particulièrement simple: on manipulera juste un objet de type SPLongOperation que l’on instancie en passant une instance de la page courante.

Code

Les propriétés LeadingHTML et TrailingHTML permettent de spécifier le texte affiché dans la page d’attente.

Sur l’appel de la méthode Begin(), l’utilisateur sera transféré sur celle-ci jusqu’à l’appel de la méthode End(). Attention donc à appeler End() quoi qu’il arrive sinon l’utilisateur restera coincé sur la page d’attente. End() prend en paramètre l’url vers laquelle l’utilisateur doit être redirigé.

image

Comme d’habitude avec les objets SharePoint, les bonnes pratiques imposent qu'on libère les ressources via la méthode Dispose(), ou en utilisant l’objet dans un bloc using.

SharePoint 2007 : Zones et URL internes

Les URL des liens internes dans SharePoint doivent dépendre la zone du site. Lorsqu' on récupère une URL par code, il faut donc faire faire attention à la zone de l' instance d' SPSite utilisée.

Toutes les URL obtenues via le modèle objet de SharePoint dépendent de cette zone, y compris les valeurs des champs de type URL tels que les liens internes ou les photos des utilisateurs.

image 

Lorsqu' on ne précise pas la zone dans le constructeur de SPSite, la zone par défaut (définie dans l' administration centrale) est utilisée. Sur la capture d'écran, on remarque que l' Extranet est configuré comme zone par défaut.

Pour récupérer des URL dans un contexte Web, on privilégiera donc l' utilisation de l' instance SPContext.Current, qui expose des objets correspondant toujours à la zone de la requête en cours.

Si la récupération de l' URL requiert l' utilisation de SPSecurity.RunWithElevatedPrivileges, on est amené à devoir instancier un SPSite différent de celui exposé par SPContext.Current. Il faut alors utiliser le constructeur disposant du paramètre SPUrlZone pour afficher une URL correcte.

 image

Un tel oubli peut être particulièrement gênant lorsqu' il s' agit d' URL d' images. D' autant que lorsqu' on développe, on n' a pas forcément le réflexe de tester nos pages en zone Extranet.


Classé sous , , ,

SharePoint : Détecter un changement de valeur de propriété Personalizable

Dans le cadre du développement d'une évolution (gestion du VB.NET) pour la CodeTesterWebPart, je me suis rendu compte que le modèle objet de la WebPart ne permet pas de savoir directement, sur le set d'une propriété personalizable, si ce set correspond à une action utilisateur (changement de valeur) ou à l'initialisation du contrôle (affectation de la valeur sauvegardée).

N'ayant pas trouvé de documentation sur le sujet, j'ai remonté les deux callstack :

  • Dans le cadre de l'initialisation du contrôle, la propriété est affectée par le SPWebPartManager sur l'événement PageInitComplete de la page.
  • Dans le cadre d'un changement de valeur de propriété par l'utilisateur, le set de la propriété est appellé depuis le RaisePostBackEvent de la page (après le load donc). D'ailleurs, si nous sommes dans ce cas, il s'agît toujours d'une requête POST.

Dans une requête POST, la méthode CreateChildControls() de la WebPart est appellé juste avant le Load de la page, c'est à dire après que la propriété soit initialisée, mais avant qu'elle puisse être changée.

Partant de cette observation, j'utilise la propriété ChildControlsCreated pour déterminer s'il s'agit d'une initialisation ou d'un changement de valeur :

Language


Classé sous , ,

Sharepoint : La CodeTesterWebPart en pratique

Lorsque l'on développe sous Sharepoint, à chaque fois que l'on modifie son code il faut compiler, packager, déployer, recycler son application pool, et attendre que Sharepoint s'initialise. On peut alors vérifier l'éxécution du code si on a la chance d'avoir un bouton qui le déclenche directement, mais celui-ci peut aussi être éxécuté en fin de workflow ou lors d'évènements plus ou moins évidents à reproduire.

Tester du code dans son contexte d'exécution est donc souvent une tâche longue et fastidieuse pour le développeur Sharepoint.

La CodeTesterWebPart permet la compilation et l'éxécution instantanée de code C#. Elle est disponible depuis quelques jours sur CodePlex : http://www.codeplex.com/CodeTesterWebPart

On dispose d'une boite de texte dans laquelle on saisit le code que l'on souhaite, et d'un bouton "Run that method!" qui permet de l'éxécuter instantanément. On peut modifier la liste des assemblies référencées (en spécifiant leur chemin sur le disque) ainsi que les usings :

ListLists

Le bouton "Save Inputs" permet de sauvegarder les références, usings, et le code grâce au concept de Personnalization.

Si il y a des erreurs de compilation, elles sont affichées :

CompilerResults

La méthode de test dispose de deux arguments permettant de tester du code d'interface graphique : la page courante et un placeholder qui est situé juste en dessous du corps de la webpart. Ici, on joue avec un diagramme de gantt et on injecte du javascript dans la page :

Gantt2

Autre avantage, le code s'exécute avec le contexte courant. On peut donc tester tout ce qui concerne les droits. Par exemple, avec un compte visiteur, si on tente de modifier le titre du site courant avec la CodeTesterWebPart, on est redirigé vers la page Access Denied. On se rend alors compte qu'il faut utiliser  SPSecurity.RunWithElevatedPrivileges, le code suivant fonctionne sans problème :

SPSecurity

On pourrait se dire que les limites sont atteintes dès lors que l'on veut tester du code récursif. Non, c'est possible aussi ! Il suffit de tricher un peu sur le parenthèsage. Après tout, le code est compilé tel qu'il est rendu à l'écran.

Recursive

Mais comment ca marche ?!

Le namespace System.CodeDom contient des classes qui permettent de compiler dynamiquement une assembly à partir de code source sous forme de chaine de caractères. La CodeTesterWebPart compile ainsi une assembly en mémoire, il n'y a pas d'écriture sur le disque. La méthode TestMethod est ensuite appellée par reflection, de manière tout à fait classique. Pour les plus curieux, le code source est disponible sur CodePlex.

Sharepoint : Développez plus vite avec la CodeTesterWebPart

Et si on pouvait vérifier que notre code s'exécute comme prévu avant même de le déployer?

Voici la CodeTesterWebPart :

CodeTesterWebPart

Téléchargement : http://www.codeplex.com/CodeTesterWebPart

Conçue dans ce but, j'espère que cette Web part vous fera gagner du temps lors de vos prochains développements :)

Sharepoint : Le contrôle ScriptLink

Le contrôle ScriptLink de Sharepoint permet d'effectuer l'enregistrement d'un fichier javascript dans une page. Voici les avantages de ce contrôle par rapport au ClientScript.RegisterClientScriptInclude() d'ASP.NET :

  • Gestion de la localisation
  • Gestion du cache
  • Le script est toujours référencé dans le header

Les deux choses importantes à savoir avant de l'utiliser sont que le chemin à spécifier doit être relatif au répertoire LAYOUTS (si utilisation sans localisation), et que si le fichier n'est pas trouvé une erreur sera levée.

Dans le cadre d'une utilisation avec localisation, Sharepoint cherchera la version qui correspond à la langue du site dans LAYOUTS/XXXX, où XXXX est le LCID du site courant (1033 pour l'anglais, 1036 pour le français).

Voyons un exemple d'utilisation avec une web part. Chacun des fichiers .js contient une méthode HelloWorld qui lance une alerte avec un message different. Voici la structure et le code du projet :

SLDemo

Dans ce cas c'est le fichier LAYOUTS/SLDemo/ScriptLinkDemo.js qui est référencé :

Neutral

Maintenant essayons avec nos versions localisées, en changeant juste la valeur de Localizable :

Localizable

Résultat à l'éxécution avec un site en francais (LCID 1036):

French

C'est la version qui correpspond au LCID du site qui est utilisée, on peut le vérifier en examinant le code HTML de la page:

Script2

Mais que se passe-t-il si il n'y a pas de fichier pour ce LCID ? Essayons. J'efface le fichier SLDemo/1036/ScriptLinkDemo.js, un coup de iisreset.exe, et je recharge ma page :

Error

On remarque que ni la version neutre ni la version anglaise n'ont été substituées par Sharepoint, et que le message d'erreur n'indique pas le chemin exact attendu. Attention donc à deployer une version par langue susceptible d'être utilisée.


Classé sous , ,

.NET 3.5 : Les certifications

Ca y est, il est possible de passer certaines des certifications du Framework .NET 3.5 !

Attention, seules les certifications relatives aux 3 briques majeures du Framework 3.0 sont prêtes, à savoir :

Les autres sont pour bientôt :

Pour préparer ces examens, on peut commencer par télécharger le training kit de Microsoft : Visual Studio 2008 and .NET Framework 3.5 Training Kit

Bonnes révisions à tous ;)

Technorati Profile

Classé sous ,

Sharepoint 2007 et Framework .NET 3.5 : Intégration d'AJAX en pratique

J'ai souhaité tester moi-même l'intégration d'AJAX dans Sharepoint. Disposant d'un Visual Studio 2008, j'en ai profité pour vérifier que le SP1 de Sharepoint règle le problème de compatibilité avec l'UpdatePanel d'AJAX.

Pré-requis, donc, un WSS mis à jour avec le Service Pack 1, et le Framework .NET 3.5.

Etape 1 : Mise à jour du Web.Config

AJAX requiert des changements assez lourds dans le Web.config de chaque application dans laquelle on l'utilise. Je me suis souvenu être tombé, il y a quelques temps, sur une feature Sharepoint permettant de mettre à jour le web.config : AJAX.Config, disponible dans le package Sharepoint Features 2007 (qui contient d'ailleurs de petits bijoux). Malheureusement, cette feature ne prend pas en compte les nouveaux numéros de version des assemblies du Framework 3.5. L'activer rend inutilisable l'application web à moins que l'ancienne version AJAX ne soit installée.

Il faut donc (à ce jour) mettre à jour le web.config manuellement, suivant la procédure de Microsoft, sans oublier de remplacer tous les numéros de version par 3.5.0.0. Hormi cela, aucun problème : notre application web Sharepoint est maintenant compatible AJAX !

Etape 2 : Modification de la master page

Pour pouvoir utiliser AJAX dans une page, il faut un et un seul ScriptManager. Il est donc recommandé de l'ajouter dans la master page du site. Sur son blog traitant du sujet, Mike suggère d'éditer manuellement le fichier master via un chemin WEBDAV (\\server\<pathtosite>\_catalogs\masterpage), et de placer la balise ScriptManager juste après la balise SPWebPartManager. Facile :

master

Ca y est, notre site est prêt pour accueillir des contrôles AJAX !

On pourrait aussi éditer le master directement dans le répertoire 12, mais puisqu'il s'agît d'un des fichiers par défaut utilisé par Sharepoint, cette manipulation n'est pas supportée par Microsoft. Le mieux en pratique est donc d'utiliser un master personnalisé.

Développement d'une WebPart AJAX

On notera dans le post de Mike le besoin d'utiliser du code qui enregistre un block de script permettant de patcher le javascript de Sharepoint. Dans une configuration WSS 3.0 SP1 + Framework 3.5, ceci est sensé être inutile. J'ai réalisé une toute petite WebPart utilisant un UpdatePanel pour prouver cette théorie :

WebPartCode

Le tour est joué, ça fonctionne !

WebPartScreenShot

Le test est vraiment basique et je compte bien aller plus loin très prochainement. Cela fera sûrement l'objet d'un autre post.


Classé sous , ,

Design d'un simulateur de poker performant grâce à Linq

Dans le cadre du développement d’un robot capable de jouer au Texas Hold’em, j’ai créé une classe RoundInfo qui centralise toutes les informations sur la main en cours. Ce post est dédié à son design car il est amusant, performant en consommation mémoire, et basé entièrement sur Linq.

 

Le projet en quelques mots

Un des gros challenges quand on développe un robot de poker est la performance. A l’ère Bruel ^^, lorsque j’ai développé mon premier robot (en appliquant point par point la méthode de la RACHE), et une fois que suffisamment d’ « intelligence » fut implémentée, il arrivait parfois qu’il mette plus de 20 secondes à réagir lorsque son tour venait. Résultat, il se faisait kicker de la table. Le proof of concept était validé, mais le design méritait d'être revu. Par manque de temps, j'ai alors abandonné le projet.

Quelques générations de micro-processeurs plus tard, lorsque j'ai souhaité tester les nouveautés du Framework 3.5, la motivation était toute trouvée pour reprendre refaire le projet. Cette fois la priorité serait mise sur la performance. Pas seulement au niveau des traitements, mais aussi au niveau mémoire car l’IA devrait conserver en mémoire toutes les mains jouées contre chacun des adversaires afin de mieux les « lire ».

La solution comporte 4 projets :

·         Un projet de type console application qui sert de point d’entrée

·         HoldemSimulator : librairie maison permettant de simuler une table de poker et la partie qui s’y déroule, ainsi que deux implémentations sommaires de IPlayer

·         HoldemInterface : contient l’interface IPlayer à implémenter pour réaliser un robot compatible avec HoldemSimulator

·         Nono le petit robot : implémentation vicieuse de IPlayer

·         HandEvaluator : une librairie C# incontournable lorsque l’on effectue des calculs de probabilité dans le contexte du Poker.

 

Les classes EventInfo

Une main de poker peut se résumer en quelques méta-datas (qui est dans la partie et à quelle place, quels sont les montants des blinds, etc.) ainsi qu’une suite ordonnée d’événements/actions (tel joueur mise tant, tel autre joueur relance, telle carte est découverte au turn, etc.). Tous ces éléments, et leur ordre, définissent une main de poker. Voici pour exemple le log d’une main jouée sur un site de poker en ligne :

Starting Hand #204916231

Game Type: Hold'em

Limit Type: No Limit

Table Type: Ring

Money Type: PLAY MONEY

Blinds are now $50/$100

Button is at seat 10

Seat 3: novembertango - $63757

Seat 4: MoneA - $10000

Seat 6: polak33 - $9000

Seat 7: Kheops - $6000

Seat 9: raceman69 - $0 (away from table)

Seat 10: john83 - $0 (away from table)

Moving Button to seat 7

novembertango posts small blind ($50)

MoneA posts big blind ($100)

Shuffling Deck

Dealing Cards

Dealing [2d Ah] to Kheops

polak33 calls $100

Kheops calls $100

novembertango calls $100

MoneA checks

Dealing Flop [As 5s Qs]

novembertango checks

MoneA checks

polak33 checks

Kheops bets $5900 (all-in)

novembertango folds

polak33 laughs his *** off.

MoneA folds

polak33 folds

Kheops shows [2d Ah]

Kheops has One Pair: Aces

Kheops wins $400

End Of Hand #204916231

 

Afin de décrire chacune de ces informations qui définissent une main, j’ai créé une classe abstraite EventInfo dont héritent tous les types d’information. Ce sont des classes toutes simples composées de rares propriétés et d'une méthode ToString(). C'est la hiérarchie des classes et le typage fort qui sont pertinents. Je vous invite à cliquer sur le diagramme de classes ci-dessous pour en voir une version lisible.

 

 ClassDiagram

 

La classe RoundInfo

 

Dans le contexte d’une utilisation dans le simulateur, cette classe est utilisée par le Dealer (objet qui distribue les cartes), par les 9 instances FishPlayer qui sont les adversaires de Nono, et par Nono en personne. Elle dispose de propriétés permettant de savoir en temps réel combien il y a dans le pot, qui est à la table, qui doit jouer, combien doit-il miser pour suivre, de combien est la relance minimum, etc. Cette classe est donc centrale et de ses performances dépend le nombre de mains que le simulateur peut traiter par seconde. C’est là qu’intervient Linq !

 

La classe est composée d’un seul accesseur, privé, une collection List<EventInfo> _events. En pratique, pour chaque méta-data et à chaque fois qu’une action a lieu à la table, le dealer appelle la méthode AddEventInfo(EventInfo info) en passant une instance d'EventInfo. De son coté, chaque robot fait de même pour maintenir sa propre instance de RoundInfo.

 

Grâce à Linq, cette unique collection permet de fournir une multitude de propriétés. En voici un tout petit extrait :

 

 

        public virtual Int32 BigBlind {

            get { return _events.OfType<PlayerBlindEventInfo>().Where(pm => pm.Type == BlindType.Big).Select(pm => pm.Chips).FirstOrDefault(); }

        }

 

        public virtual String Board {

            get { return _events.OfType<BoardEventInfo>().Select(be => be.Board).LastOrDefault() ; }

        }

 

        public virtual String[] Players {

            get { return _events.OfType<PlayerStartEventInfo>().OrderBy(ps => ps.Seat).Select(ps => ps.PlayerId).ToArray(); }

        }

 

        public virtual StepName CurrentStep {

            get { return _events.OfType<BoardEventInfo>().Select(be => be.Step).LastOrDefault(); }

        }

 

        public virtual String[] RemainingPlayers {

            get { return Events.OfType<PlayerStartEventInfo>().Where(ps => !HasFolded(ps.PlayerId)).OrderBy(ps => (ps.Seat - ButtonSeat - 1 + Utils.SeatsPerTable) % Utils.SeatsPerTable).Select(ps => ps.PlayerId).ToArray(); }

        }

 

        public virtual String[] AllInPlayers {

            get { return Events.OfType<PlayerBetEventInfo>().Where(pb => pb.AllIn).Select(pb => pb.PlayerId).Distinct().ToArray(); }

        }

 

        public virtual String NextPlayer {

            get { return GetNextPlayer(); }

        }

 

        public virtual Int32 ButtonSeat {

            get { return Events.OfType<ButtonEventInfo>().Select(b => b.Seat).Single(); }

        }

 

Le diagramme des classes touffu d'EventInfo prend tout son sens lorsque l'on voit l'utilisation massive de de la méthode OfType<T>() qui ne retourne que les objets du type T .

 

Les rares méthodes privées de la classe sont elles aussi basées sur Linq :

 

 

        private Boolean HasFolded(String playerId) {

 

            return Events.OfType<PlayerFoldEventInfo>().Select(pf => pf.PlayerId).Contains(playerId);

 

        }

 

        private String GetNextPlayer() {

 

            if (Events.Last() is BoardEventInfo)

                return RemainingPlayers.First();

 

            //Last player that moved

            String lastPlayerName = Events.OfType<PlayerMoveEventInfo>().Except(Events.OfType<PlayerFoldEventInfo>().ToArray()).Select(pm => pm.PlayerId).LastOrDefault();

 

            //if null, the next player is the small blind

            if (String.IsNullOrEmpty(lastPlayerName))

                return (RemainingPlayers.Length == 2) ? RemainingPlayers.LastOrDefault() : RemainingPlayers.FirstOrDefault();

 

            //Seat of the last player that moved

            Int32 lastPlayerSeat = Events.OfType<PlayerStartEventInfo>().Where(ps => ps.PlayerId == lastPlayerName).Select(ps => ps.Seat).FirstOrDefault();

 

            String result = Events.OfType<PlayerStartEventInfo>().Where(ps => !HasFolded(ps.PlayerId) && !IsAllIn(ps.PlayerId)).OrderBy(ps => (ps.Seat - lastPlayerSeat - 1 + Utils.SeatsPerTable) % Utils.SeatsPerTable).Select(ps => ps.PlayerId).First();

 

            return result;

 

        }

 

Terminé les 38 collections (souvenir de ma V1) à maintenir en permanence, on requête dynamiquement une seule et unique collection! D’un point de vue mémoire c’est du tout bon car on a l’historique et le statut en temps réel, le tout grâce à une seule collection. Et d’un point de vue maintenance c’est vraiment agréable, le moindre problème s'identifie très facilement.

Certes, la même approche aurait pu être envisagée sans Linq, mais le code aurait été beaucoup trop volumineux et incompréhensible pour que tout cela soit réaliste.

Les seuls reproches que je fais à RoundInfo sont le fait que la classe ne soit pas thread-safe, et le coût processeur injustifié lors d’accès répétés à une même propriété entre deux appels à AddEventInfo().

 

Une brève revue de code met en évidence que ce cas ne se présente qu’au moment ou le robot doit prendre une décision. C’est là qu’intervient la classe RoundSnapShot.

 

 

La classe RoundSnapShot

 

Au moment de la prise de décision par le robot (implémentée sous la forme d’un réseau de neurones travaillant en asynchrone), tous les neurones accèderont aux propriétés de l'objet RoundInfo, ce qui peut conduire à une grosse déception côté performance. On va donc utiliser RoundSnapShot, une image de RoundInfo à un instant donné obtenue avec la méthode publique GetSnapShot() de la classe RoundInfo. RoundSnapShot hérite de RoundInfo et surcharge chacune de ses propriétés de la manière suivante :

 

        private Int32? _smallBlind;

 

        public override Int32 SmallBlind {

            get {

                lock (_lockSmallBlind)

                   if (_smallBlind == null)

                       _smallBlind = base.SmallBlind;

                return (Int32)_smallBlind;

            }

        }

 

        private String[] _allInPlayers;

 

        public override String[] AllInPlayers {

            get {

                lock (_lockAllInPlayers)

                   if (_allInPlayers == null)

                      _allInPlayers = base.AllInPlayers;

                return _allInPlayers;

            }

        }

        //et ainsi de suite..

 

J’ai déclaré un objet lock différent pour chaque propriété car les différents neurones accèdent à plusieurs propriétés en même temps, donc un seul objet lock partagé par toutes les propriétés aurait ralenti inutilement la prise de décision.

 

 

Un log simple à implémenter

 

Afin de pouvoir étudier le comportement du robot, et tout simplement débugger le simulateur, j’avais besoin d’un moyen de générer un log similaire à celui d’un site de poker en ligne décrit plus haut, afin de voir l'historique de la main d'un simple coup d'oeil. Avec ce design, rien de plus simple ! J’ai surchargé la méthode ToString() de chaque classe héritant de EventInfo.

 

Exemples :

 

         //BoardEventInfo

        public override string ToString() {

            if(Step == StepName.Flop)

                return String.Format("Flop : [{0}]", Board);

            else

                return String.Format("{0} : [{1}]", Step, Board.Split(' ').Last());

        }

 

        //PlayerRaiseEventInfo

        public override string ToString() {

            return String.Format("{0} raises {1}{2}", PlayerId, Chips, AllIn ? " and is all-in" : null);

        }

 

Pour atteindre mon but final, j’ai aussi surchargé ToString() dans RoundInfo :

 

        public override string ToString() {

            StringBuilder builder = new StringBuilder();

            foreach (EventInfo info in Events)

                builder.AppendLine(info.ToString());

            return builder.ToString();

        }

 

Il n’y a plus qu’à utiliser les classes Trace et Debug du framework pour écrire le log de la main dans un fichier texte ou dans la console de debug.

 

RoundInfoToString

 

Promis, la prochaine fois je parlerai Sharepoint :)

Sharepoint : Développement d'un template de site contenant une colonne lookup

Les colonnes lookup permettent aux éléments d'une liste de référencer un ou plusieurs éléments d'une autre liste, afin de s'approcher du comportement d'une base de données relationnelle dans WSS 3.0. Créer une colonne lookup dans un site éxistant n'est pas particulièrement difficile, mais dans le contexte d'un template de site, on coince car une colonne lookup ne peut être crée que si la liste cible existe : lors de la définition d'un template de site accompagné de templates de listes on ne peut pas connaitre les identifiants qu'auront ses listes une fois créées.

La technique que nous allons voir consiste en une feature qui définit et instancie les 2 listes, puis créé la colonne lookup.

Nous allons réaliser un template de site permettant le suivi des résultats de parties de poker, contenant une liste Parties et une liste Résultats. Un résultat est le gain net d'un joueur (utilisateur membre du site) lors d'une partie, on voudra donc une colonne lookup dans la liste Résultats. On utilisera un projet de type WSPBuilder (installé avec WSPBuilder Extensions) afin de ne pas se préoccuper du packaging et du déploiement, et Sharepoint Site Generator disponible dans le package Windows SharePoint Services 3.0 Tools: Visual Studio 2005 Extensions.

Créons un site vierge avec l'interface classique de Sharepoint, et ajoutons-y les listes et colonnes que l'on souhaite avoir dans notre template. Utilisons ensuite Sharepoint Site Generator pour générer les templates de ces deux listes : Les répertoires Parties et Resultats sont créés.

Création de la feature des templates de liste

Ajoutons à notre projet une feature vierge (via WSPBuilder) de scope Web pour nos templates de liste et copions y les 2 répertoires correspondants fraichement générés. En observant Resultats/schema.xml, on se rend bien compte du problème : la colonne lookup n'a pas pu être définie automatiquement :

NoLookupCol

Bien que la colonne Partie n'existe pas, les vues (éléments View) la référencent. Gardez cela en mémoire, nous en reparlerons lors de la dernière étape.

Afin que nos deux templates de listes soient inclus dans la feature et instanciés lors de son activation, il faut remplir elements.xml comme ceci :

Elements.xml

ListTemplate : Nous devons décider d'un identifiant Type pour chacun de nos templates de liste. Ceux-ci doivent être uniques dans la feature dans laquelle ils sont déclarés. Prenons donc 1 et 2. Hidden="TRUE" permet de ne pas proposer nos templates de liste dans l'écran accessible via le bouton "Create" de WSS. Cliquez ici pour plus d'informations sur l'élément ListTemplate.

ListInstance : L'outil CreateGuid de visual studio nous permet de générer des identifiants pour nos deux instances. la valeur de FeatureId doit correspondre à l'identifiant de la feature générée par WSPBuilder (visible dans feature.xml), et celle de Type doit correspondre celle du ListTemplate correspondant. Cliquez ici pour plus d'informations sur l'élément ListInstance.

Création d'un template de site

Afin de créer un template de site vierge, copions le contenu du template de site de base "sts" (répertoire C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\SiteTemplates) dans notre projet sous 12/TEMPLATE/SiteTemplates/PokerLookup. Ouvrons le fichier onet.xml et dans le noeud <Configuration ID="1" ..> ajoutons une référence vers notre feature :

onet

Ainsi, à chaque fois qu'une instance de notre template de site sera créée, la feature PokerLookup y sera activée, ce qui aura pour résultat la création de nos deux listes.

Le template est prêt, reste à informer Sharepoint qu'il existe. Pour ce faire, Ajoutons à notre projet un fichier 12/TEMPLATE/XXXX/XML/WEBTEMP_PokerLookup.xml, où XXXX est le LCID de la langue dans laquelle le template doit être référencé (1033 pour l'anglais, 1036 pour le francais, etc.). Sharepoint cherche la liste des templates de site à chaque fois qu'il redémarre dans les fichiers WEBTEMP_*.xml de tous les répertoires 12/TEMPLATE/XXXX/XML.

Il faut ensuite référencer la configuration 1 du template de site PokerLookup dans notre fichier WEBTEMP_PokerLookup.xml:

WebTemp

Notre template de site est prêt pour un premier test. A ce stade, la structure du projet est la suivante :

Project

Grâce au menu contextuel offert par WSPBuilder, on peut packager et déployer notre solution. Il est alors possible de créer un site avec notre template de site : les 2 listes apparaissent correctement. Cependant, il manque la colonne lookup Partie dans la liste Résultats pour finaliser notre template.

Ajout de la colonne lookup

Il existe deux moyen de procéder : par code avec un SPFeatureReceiver, et en manipulant le fichier schema.xml.

Ajout d'une colonne lookup par code

Le SPFeatureReceiver permet d'éxécuter du code après qu'une feature soit activée. Créons une classe PokerLookup_Lists, héritant de SPFeatureReceiver, et ajoutons-y le code suivant :

FeatureReceiver

Indiquons dans feature.xml que cette classe est branchée à notre feature :

Feature

On notera l'attribut Hidden="TRUE" dont le but est d'éviter que des administrateurs puissent réactiver cette feature, ce qui pourrait poser problème puisque le code de FeatureActivated n'est prévu pour être exécuté qu'une seule fois (afin de ne pas s'éloigner du sujet de ce post).

Si l'on redéploie notre solution et que l'on créé une nouvelle instance de notre template de site, nous constaterons que la colonne Partie est correctement crée. Cependant, elle est présente en double dans les vues définies par notre template de liste. Lors d'une telle implémentation, si on a utilisé Sharepoint Site Generator pour créer les templates de liste, il faut donc retirer "à la main" toutes les références à la colonne lookup dans le fichier schema.xml pour éviter ce problème. 

Ajout d'une colonne lookup par XML

Créer un SPFeatureReceiver juste pour ajouter une colonne lookup fait forcément froncer les sourcils au développeur Sharepoint, il existe heureusement une solution XML. La définition du champ Partie (dans Resultats/schema.xml, qui est commentée par Sharepoint Site Generator, est en fait utilisable si on utilise l'attribut List pour spécifier l'url de la liste cible (au lieu de son Id dans son contexte d'origine) :

schemaResultats

Bien que le SDK spécifie d'utiliser l'attribut List pour spécifier le nom de la liste cible, il faut bel et bien spécifier l'url. Cliquez ici pour plus d'informations sur l'élément Field.

Conclusion

Les colonnes lookup, indépendantes du concept de template de site, sont plus difficiles à intégrer aux templates de site que les colonnes classiques. Le plus pratique est alors d'utiliser une feature regroupant les deux listes (et leurs templates) concernées.

Déclarer la colonne dans le fichier schema.xml du template de liste permet de centraliser toutes les déclarations de colonnes et rend le template plus maintenable, ne nécessitant pas le déploiement d'une assembly. Mais dans des cas plus complexe où l'url de la liste cible n'est pas toujours connue (colonne lookup cross-site), ou autres cas tordus, il peut s'avérer utile d'utiliser la déclaration par code qui offre plus de possibilités et de souplesse.

Plus de Messages Page suivante »

Les 10 derniers blogs postés

- TechDays Paris 2010 : La BI dans SharePoint 2010 par Blog Technique de Romelard Fabrice le il y a 1 heure et 17 minutes

- TechDays Paris 2010 : Déploiement de nouvelles technologies – Retour d’expérience par l’informatique de Microsoft par Blog Technique de Romelard Fabrice le il y a 2 heures et 44 minutes

- TechDays Paris 2010 : Plan de migration vers SharePoint 2010 par Blog Technique de Romelard Fabrice le il y a 6 heures et 27 minutes

- TechDays Paris 2010 : La pleinière du second jour par Blog Technique de Romelard Fabrice le il y a 7 heures et 32 minutes

- Visual Studio 2010 and .NET Framework 4 Release Candidate now available par Matthieu MEZIL le il y a 10 heures et 38 minutes

- Création d’une base de donnée sous SQL Azure par Le Blog (Vert) d'Arnaud JUND le il y a 11 heures et 35 minutes

- TechDays Paris 2010 : Les Services d’applications dans SharePoint 2010 par Blog Technique de Romelard Fabrice le il y a 21 heures et 34 minutes

- TechDays Paris 2010 : La GED et SharePoint 2010 par Blog Technique de Romelard Fabrice le 02-08-2010, 16:54

- TechDays Paris 2010 : SharePoint 2010 et Les réseaux sociaux par Blog Technique de Romelard Fabrice le 02-08-2010, 15:40

- TechDays Paris 2010 : SharePoint 2010 – Description et nouveautés par Blog Technique de Romelard Fabrice le 02-08-2010, 14:33