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.

Salut Codes-Sources !

Bonjour à tous !

Ceci étant mon premier message, voici la traditionnelle présentation : Je travaille en tant que développeur-consultant .NET chez Winwise depuis bientôt 2 ans, et depuis quelques mois au sein du pôle collaboratif. 

Je ne prévois pas de limiter ce blog à Sharepoint 2007, et compte bien poster quelques billets au sujet de mes futures découvertes sur WCF, Linq, WPF, et autres technologies incontournables du monde .NET. Joueur occasionnel, il est possible que certains de mes exemples de code aient comme un parfum de poker ;)

Je tiens à remercier Phil, Gribouillon, Azra, Daniel, et bien sûr Cyril, qui ont tous contribué à l'ouverture de ce blog (Faut pas croire, c'est pas aussi facile que ca en a l'air ^^).

A bientôt sur codes-sources,

Arnault


Les 10 derniers blogs postés

- [Refactoring] ReSharper pour Visual Studio 2010 (Preview) par Thomas Jaskula le il y a 4 heures et 32 minutes

- [Refactoring] Analyser vos exceptions avec ReSharper Exceptional par Thomas Jaskula le il y a 5 heures et 46 minutes

- SharePoint 2007 : patterns & practices SharePoint Guidance par Philippe Sentenac [MVP SharePoint] le il y a 19 heures et 26 minutes

- [Visual Studio 2010] Les tests cases c’est bien, mais je vais devoir tout réécrire ? par Etienne Margraff le il y a 20 heures et 22 minutes

- MVP[Gribouillon].AddYear par The Grib's Lair [Sébastien PICAMELOT - MVP SharePoint] le il y a 20 heures et 37 minutes

- Clinique INSIA - Projet de fin d’Etudes (Silverlight 3 MVVM et OutOfBrowser, WCF, TFS) - Part 1 par David REI le 07-02-2009, 23:38

- C’est la crise ? Bah pourquoi cramer du budget pub alors ? par Nix's Blog le 07-02-2009, 15:31

- Soyons MVP ! par TheSaib .NET blog le 07-02-2009, 12:15

- SharePoint : Gestion des Erreurs 6398, 7076 et 6482 par Blog Technique de Romelard Fabrice le 07-02-2009, 11:53

- EF avec WPF par Matthieu MEZIL le 07-02-2009, 10:18